Compare commits
28 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08e35f209d | ||
|
|
55a043366a | ||
|
|
864a37f1e6 | ||
|
|
72c7c94f3d | ||
|
|
c3a9b006d2 | ||
|
|
b37397acb8 | ||
|
|
49c3b14bf0 | ||
|
|
d2cba48f8e | ||
|
|
f752c90058 | ||
|
|
a78d5593f2 | ||
|
|
079208a1bd | ||
|
|
4721582dfe | ||
|
|
1746eec5da | ||
|
|
f9cf86a17c | ||
|
|
49ef846ebd | ||
|
|
b3b7dea930 | ||
|
|
196e074ef5 | ||
|
|
489027991c | ||
|
|
d445553d85 | ||
|
|
e9fc41541b | ||
|
|
c474bd2fc0 | ||
|
|
d6a1f9c28e | ||
|
|
5be13d1602 | ||
|
|
053217d3e4 | ||
|
|
493dc56892 | ||
|
|
a38ba93724 | ||
|
|
0029901ca3 | ||
|
|
1381533b9c |
18
.github/workflows/publish-wiki.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Publish wiki
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- wiki/**
|
||||
- .github/workflows/publish-wiki.yml
|
||||
concurrency:
|
||||
group: publish-wiki
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
publish-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
3
.github/workflows/rust-build-test.yaml
vendored
@@ -16,6 +16,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --verbose
|
||||
|
||||
|
||||
4
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
target/*
|
||||
target/*
|
||||
**/node_modules/*
|
||||
.DS_Store
|
||||
|
||||
982
Cargo.lock
generated
@@ -4,6 +4,8 @@ resolver = "2"
|
||||
members = [
|
||||
"base",
|
||||
"xlsx",
|
||||
"tironcalc",
|
||||
"bindings/wasm",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
|
||||
14
Makefile
@@ -7,14 +7,18 @@ format:
|
||||
|
||||
tests: lint
|
||||
cargo test
|
||||
make remove-xlsx
|
||||
./target/debug/documentation
|
||||
cmp functions.md wiki/functions.md || exit 1
|
||||
make remove-artifacts
|
||||
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs
|
||||
|
||||
remove-xlsx:
|
||||
remove-artifacts:
|
||||
rm -f xlsx/hello-calc.xlsx
|
||||
rm -f xlsx/hello-styles.xlsx
|
||||
rm -f xlsx/widths-and-heights.xlsx
|
||||
rm -f functions.md
|
||||
|
||||
clean: remove-xlsx
|
||||
clean: remove-artifacts
|
||||
cargo clean
|
||||
rm -r -f base/target
|
||||
rm -r -f xlsx/target
|
||||
@@ -27,6 +31,10 @@ coverage:
|
||||
CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
|
||||
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
|
||||
|
||||
update-docs:
|
||||
cargo build
|
||||
./target/debug/documentation -o wiki/functions.md
|
||||
|
||||
docs:
|
||||
cargo doc --no-deps
|
||||
|
||||
|
||||
52
README.md
@@ -117,55 +117,7 @@ See more examples in the `examples` folder of the xlsx crate.
|
||||
|
||||
# ROADMAP
|
||||
|
||||
> [!WARNING]
|
||||
> This is work-in-progress. IronCalc in developed in the open. Expect things to be broken and change quickly until version 0.5
|
||||
|
||||
Major milestones:
|
||||
|
||||
* MVP, version 0.5.0: We intend to have a working version by mid March 2024 (version 0.5, MVP)
|
||||
* Stable, version 1.0.0 will come later in December 2024
|
||||
|
||||
MVP stands for _Minimum Viable Product_
|
||||
|
||||
## Version 0.5 or MVP (early 2024)
|
||||
|
||||
Version 0.5 includes the engine, javascript and nodejs bindings and a web application
|
||||
|
||||
Features of the engine include:
|
||||
|
||||
* Read and write xlsx files
|
||||
* API to set and read values from cells
|
||||
* Implemented 192 Excel functions
|
||||
* Time functions with timezones
|
||||
* Prepared for i18n but will only support English
|
||||
* Wide test coverage
|
||||
|
||||
UI features of the web application (backed by the engine):
|
||||
|
||||
* Enter values and formulas. Browse mode
|
||||
* Italics, bold, underline, horizontal alignment
|
||||
* Number formatting
|
||||
* Add/remove/rename sheets
|
||||
* Copy/Paste extend values
|
||||
* Keyboard navigation
|
||||
* Delete/Add rows and columns
|
||||
* Resize rows and columns
|
||||
* Correct scrolling and navigation
|
||||
|
||||
## Version 1.0 or Stable (December 2024)
|
||||
|
||||
Minor milestones in the ROADMAD for version 1.0.0 (engine and UI):
|
||||
|
||||
* Implementation of arrays and array formulas
|
||||
* Formula documentation and context help
|
||||
* Merge cells
|
||||
* Pivot tables
|
||||
* Define name manager (mostly UI)
|
||||
* Update main evaluation algorithm with a support graph
|
||||
* Dynamic arrays (SORT, UNIQUE, ..)
|
||||
* Full i18n support with different locales and languages
|
||||
* Python bindings
|
||||
* Full test coverage
|
||||
See https://github.com/ironcalc
|
||||
|
||||
# Early testing
|
||||
|
||||
@@ -192,4 +144,4 @@ Licensed under either of
|
||||
* [MIT license](LICENSE-MIT)
|
||||
* [Apache license, version 2.0](LICENSE-Apache-2.0)
|
||||
|
||||
at your option.
|
||||
at your option.
|
||||
|
||||
BIN
assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 441 B |
BIN
assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 15 KiB |
BIN
assets/icon/ironcalc_icon.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
8
assets/icon/ironcalc_icon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="600" height="600" rx="20" fill="#F2994A"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 33 KiB |
BIN
assets/logo/png/black.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/logo/png/orange+black.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/logo/png/orange+white.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/logo/png/white.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
8
assets/logo/svg/black.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/orange+black.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/orange+white.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/white.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ironcalc_base"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
homepage = "https://www.ironcalc.com"
|
||||
@@ -12,18 +12,20 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
ryu = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono-tz = "0.7.0"
|
||||
chrono-tz = "0.9"
|
||||
regex = "1.0"
|
||||
once_cell = "1.16.0"
|
||||
bitcode = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.60" }
|
||||
js-sys = { version = "0.3.69" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rand = "0.8.4"
|
||||
rand = "0.8.5"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc_base::{model::Model, types::CellType};
|
||||
use ironcalc_base::{types::CellType, Model};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc_base::{cell::CellValue, model::Model};
|
||||
use ironcalc_base::{cell::CellValue, Model};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("hello-world", "en", "UTC")?;
|
||||
|
||||
@@ -77,13 +77,13 @@ impl Model {
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.cell_formula(sheet, source_row, source_column)?
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.delete_cell(sheet, source_row, source_column)?;
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -157,6 +157,11 @@ impl Model {
|
||||
return Err("Please use insert columns instead".to_string());
|
||||
}
|
||||
|
||||
// first column being deleted
|
||||
let column_start = column;
|
||||
// last column being deleted
|
||||
let column_end = column + column_count - 1;
|
||||
|
||||
// Move cells
|
||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
@@ -166,11 +171,11 @@ impl Model {
|
||||
for r in all_rows {
|
||||
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
||||
for col in columns {
|
||||
if col >= column {
|
||||
if col >= column + column_count {
|
||||
if col >= column_start {
|
||||
if col > column_end {
|
||||
self.move_cell(sheet, r, col, r, col - column_count)?;
|
||||
} else {
|
||||
self.delete_cell(sheet, r, col)?;
|
||||
self.cell_clear_all(sheet, r, col)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +189,64 @@ impl Model {
|
||||
delta: -column_count,
|
||||
}),
|
||||
);
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
// deletes all the column styles
|
||||
let mut new_columns = Vec::new();
|
||||
for col in worksheet.cols.iter_mut() {
|
||||
// range under study
|
||||
let min = col.min;
|
||||
let max = col.max;
|
||||
// In the diagram:
|
||||
// |xxxxx| range we are studying [min, max]
|
||||
// |*****| range we are deleting [column_start, column_end]
|
||||
// we are going to split it in three big cases:
|
||||
// ----------------|xxxxxxxx|-----------------
|
||||
// -----|*****|------------------------------- Case A
|
||||
// -------|**********|------------------------ Case B
|
||||
// -------------|**************|-------------- Case C
|
||||
// ------------------|****|------------------- Case D
|
||||
// ---------------------|**********|---------- Case E
|
||||
// -----------------------------|*****|------- Case F
|
||||
if column_start < min {
|
||||
if column_end < min {
|
||||
// Case A
|
||||
// We displace all columns
|
||||
let mut new_column = col.clone();
|
||||
new_column.min = min - column_count;
|
||||
new_column.max = max - column_count;
|
||||
new_columns.push(new_column);
|
||||
} else if column_end < max {
|
||||
// Case B
|
||||
// We displace the end
|
||||
let mut new_column = col.clone();
|
||||
new_column.min = column_start;
|
||||
new_column.max = max - column_count;
|
||||
new_columns.push(new_column);
|
||||
} else {
|
||||
// Case C
|
||||
// skip this, we are deleting the whole range
|
||||
}
|
||||
} else if column_start <= max {
|
||||
if column_end <= max {
|
||||
// Case D
|
||||
// We displace the end
|
||||
let mut new_column = col.clone();
|
||||
new_column.max = max - column_count;
|
||||
new_columns.push(new_column);
|
||||
} else {
|
||||
// Case E
|
||||
let mut new_column = col.clone();
|
||||
new_column.max = column_start - 1;
|
||||
new_columns.push(new_column);
|
||||
}
|
||||
} else {
|
||||
// Case F
|
||||
// No action required
|
||||
new_columns.push(col.clone());
|
||||
}
|
||||
}
|
||||
worksheet.cols = new_columns;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -283,7 +346,7 @@ impl Model {
|
||||
// remove all cells in row
|
||||
// FIXME: We could just remove the entire row in one go
|
||||
for column in columns {
|
||||
self.delete_cell(sheet, r, column)?;
|
||||
self.cell_clear_all(sheet, r, column)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use crate::{
|
||||
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
/// A CellValue is the representation of the cell content.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CellValue {
|
||||
None,
|
||||
String(String),
|
||||
@@ -14,17 +11,6 @@ pub enum CellValue {
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl CellValue {
|
||||
pub fn to_json_str(&self) -> String {
|
||||
match &self {
|
||||
CellValue::None => "null".to_string(),
|
||||
CellValue::String(s) => json!(s).to_string(),
|
||||
CellValue::Number(f) => json!(f).to_string(),
|
||||
CellValue::Boolean(b) => json!(b).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for CellValue {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Number(value)
|
||||
|
||||
@@ -308,9 +308,9 @@ impl Lexer {
|
||||
return self.consume_range(None);
|
||||
}
|
||||
let name_upper = name.to_ascii_uppercase();
|
||||
if name_upper == self.language.booleans.true_value {
|
||||
if name_upper == self.language.booleans.r#true {
|
||||
return TokenType::Boolean(true);
|
||||
} else if name_upper == self.language.booleans.false_value {
|
||||
} else if name_upper == self.language.booleans.r#false {
|
||||
return TokenType::Boolean(false);
|
||||
}
|
||||
if self.mode == LexerMode::A1 {
|
||||
@@ -660,8 +660,8 @@ impl Lexer {
|
||||
fn consume_error(&mut self) -> TokenType {
|
||||
let errors = &self.language.errors;
|
||||
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
|
||||
if rest_of_formula.starts_with(&errors.ref_value) {
|
||||
self.position += errors.ref_value.chars().count() - 1;
|
||||
if rest_of_formula.starts_with(&errors.r#ref) {
|
||||
self.position += errors.r#ref.chars().count() - 1;
|
||||
return TokenType::Error(Error::REF);
|
||||
} else if rest_of_formula.starts_with(&errors.name) {
|
||||
self.position += errors.name.chars().count() - 1;
|
||||
|
||||
@@ -6,11 +6,11 @@ use crate::{
|
||||
token::TokenType,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale_fix,
|
||||
locale::get_locale,
|
||||
};
|
||||
|
||||
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
|
||||
let locale = get_locale_fix(locale).unwrap();
|
||||
let locale = get_locale(locale).unwrap();
|
||||
let language = get_language(language).unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ impl Parser {
|
||||
|
||||
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
|
||||
self.lexer.set_formula(formula);
|
||||
self.context = context.clone();
|
||||
self.context.clone_from(context);
|
||||
self.parse_expr()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::language::Language;
|
||||
|
||||
@@ -80,8 +80,7 @@ impl fmt::Display for OpProduct {
|
||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||
/// Note that they are serialized/deserialized by index
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Error {
|
||||
REF,
|
||||
NAME,
|
||||
@@ -119,7 +118,7 @@ impl Error {
|
||||
pub fn to_localized_error_string(&self, language: &Language) -> String {
|
||||
match self {
|
||||
Error::NULL => language.errors.null.to_string(),
|
||||
Error::REF => language.errors.ref_value.to_string(),
|
||||
Error::REF => language.errors.r#ref.to_string(),
|
||||
Error::NAME => language.errors.name.to_string(),
|
||||
Error::VALUE => language.errors.value.to_string(),
|
||||
Error::DIV => language.errors.div.to_string(),
|
||||
@@ -136,7 +135,7 @@ impl Error {
|
||||
|
||||
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
|
||||
let errors = &language.errors;
|
||||
if name == errors.ref_value {
|
||||
if name == errors.r#ref {
|
||||
return Some(Error::REF);
|
||||
} else if name == errors.name {
|
||||
return Some(Error::NAME);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Datelike;
|
||||
use chrono::Months;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Timelike;
|
||||
|
||||
use crate::expressions::types::CellReferenceIndex;
|
||||
@@ -258,8 +257,8 @@ impl Model {
|
||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(dt) => dt,
|
||||
let local_time = match DateTime::from_timestamp(seconds, 0) {
|
||||
Some(dt) => dt.with_timezone(&self.tz),
|
||||
None => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -268,7 +267,6 @@ impl Model {
|
||||
}
|
||||
}
|
||||
};
|
||||
let local_time = self.tz.from_utc_datetime(&dt);
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
@@ -289,8 +287,8 @@ impl Model {
|
||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(dt) => dt,
|
||||
let local_time = match DateTime::from_timestamp(seconds, 0) {
|
||||
Some(dt) => dt.with_timezone(&self.tz),
|
||||
None => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
@@ -299,7 +297,6 @@ impl Model {
|
||||
}
|
||||
}
|
||||
};
|
||||
let local_time = self.tz.from_utc_datetime(&dt);
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
|
||||
@@ -165,7 +165,8 @@ impl Model {
|
||||
message: "argument must be a reference to a single cell".to_string(),
|
||||
};
|
||||
}
|
||||
let is_formula = if let Ok(f) = self.cell_formula(left.sheet, left.row, left.column) {
|
||||
let is_formula = if let Ok(f) = self.get_cell_formula(left.sheet, left.row, left.column)
|
||||
{
|
||||
f.is_some()
|
||||
} else {
|
||||
false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use core::fmt;
|
||||
use std::array::IntoIter;
|
||||
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
@@ -244,6 +245,206 @@ pub enum Function {
|
||||
Subtotal,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub fn into_iter() -> IntoIter<Function, 192> {
|
||||
[
|
||||
Function::And,
|
||||
Function::False,
|
||||
Function::If,
|
||||
Function::Iferror,
|
||||
Function::Ifna,
|
||||
Function::Ifs,
|
||||
Function::Not,
|
||||
Function::Or,
|
||||
Function::Switch,
|
||||
Function::True,
|
||||
Function::Xor,
|
||||
Function::Sin,
|
||||
Function::Cos,
|
||||
Function::Tan,
|
||||
Function::Asin,
|
||||
Function::Acos,
|
||||
Function::Atan,
|
||||
Function::Sinh,
|
||||
Function::Cosh,
|
||||
Function::Tanh,
|
||||
Function::Asinh,
|
||||
Function::Acosh,
|
||||
Function::Atanh,
|
||||
Function::Abs,
|
||||
Function::Pi,
|
||||
Function::Sqrt,
|
||||
Function::Sqrtpi,
|
||||
Function::Atan2,
|
||||
Function::Power,
|
||||
Function::Max,
|
||||
Function::Min,
|
||||
Function::Product,
|
||||
Function::Rand,
|
||||
Function::Randbetween,
|
||||
Function::Round,
|
||||
Function::Rounddown,
|
||||
Function::Roundup,
|
||||
Function::Sum,
|
||||
Function::Sumif,
|
||||
Function::Sumifs,
|
||||
Function::Choose,
|
||||
Function::Column,
|
||||
Function::Columns,
|
||||
Function::Index,
|
||||
Function::Indirect,
|
||||
Function::Hlookup,
|
||||
Function::Lookup,
|
||||
Function::Match,
|
||||
Function::Offset,
|
||||
Function::Row,
|
||||
Function::Rows,
|
||||
Function::Vlookup,
|
||||
Function::Xlookup,
|
||||
Function::Concatenate,
|
||||
Function::Exact,
|
||||
Function::Value,
|
||||
Function::T,
|
||||
Function::Valuetotext,
|
||||
Function::Concat,
|
||||
Function::Find,
|
||||
Function::Left,
|
||||
Function::Len,
|
||||
Function::Lower,
|
||||
Function::Mid,
|
||||
Function::Right,
|
||||
Function::Search,
|
||||
Function::Text,
|
||||
Function::Trim,
|
||||
Function::Upper,
|
||||
Function::Isnumber,
|
||||
Function::Isnontext,
|
||||
Function::Istext,
|
||||
Function::Islogical,
|
||||
Function::Isblank,
|
||||
Function::Iserr,
|
||||
Function::Iserror,
|
||||
Function::Isna,
|
||||
Function::Na,
|
||||
Function::Isref,
|
||||
Function::Isodd,
|
||||
Function::Iseven,
|
||||
Function::ErrorType,
|
||||
Function::Isformula,
|
||||
Function::Type,
|
||||
Function::Sheet,
|
||||
Function::Average,
|
||||
Function::Averagea,
|
||||
Function::Averageif,
|
||||
Function::Averageifs,
|
||||
Function::Count,
|
||||
Function::Counta,
|
||||
Function::Countblank,
|
||||
Function::Countif,
|
||||
Function::Countifs,
|
||||
Function::Maxifs,
|
||||
Function::Minifs,
|
||||
Function::Year,
|
||||
Function::Day,
|
||||
Function::Month,
|
||||
Function::Eomonth,
|
||||
Function::Date,
|
||||
Function::Edate,
|
||||
Function::Today,
|
||||
Function::Now,
|
||||
Function::Pmt,
|
||||
Function::Pv,
|
||||
Function::Rate,
|
||||
Function::Nper,
|
||||
Function::Fv,
|
||||
Function::Ppmt,
|
||||
Function::Ipmt,
|
||||
Function::Npv,
|
||||
Function::Mirr,
|
||||
Function::Irr,
|
||||
Function::Xirr,
|
||||
Function::Xnpv,
|
||||
Function::Rept,
|
||||
Function::Textafter,
|
||||
Function::Textbefore,
|
||||
Function::Textjoin,
|
||||
Function::Substitute,
|
||||
Function::Ispmt,
|
||||
Function::Rri,
|
||||
Function::Sln,
|
||||
Function::Syd,
|
||||
Function::Nominal,
|
||||
Function::Effect,
|
||||
Function::Pduration,
|
||||
Function::Tbillyield,
|
||||
Function::Tbillprice,
|
||||
Function::Tbilleq,
|
||||
Function::Dollarde,
|
||||
Function::Dollarfr,
|
||||
Function::Ddb,
|
||||
Function::Db,
|
||||
Function::Cumprinc,
|
||||
Function::Cumipmt,
|
||||
Function::Besseli,
|
||||
Function::Besselj,
|
||||
Function::Besselk,
|
||||
Function::Bessely,
|
||||
Function::Erf,
|
||||
Function::ErfPrecise,
|
||||
Function::Erfc,
|
||||
Function::ErfcPrecise,
|
||||
Function::Bin2dec,
|
||||
Function::Bin2hex,
|
||||
Function::Bin2oct,
|
||||
Function::Dec2Bin,
|
||||
Function::Dec2hex,
|
||||
Function::Dec2oct,
|
||||
Function::Hex2bin,
|
||||
Function::Hex2dec,
|
||||
Function::Hex2oct,
|
||||
Function::Oct2bin,
|
||||
Function::Oct2dec,
|
||||
Function::Oct2hex,
|
||||
Function::Bitand,
|
||||
Function::Bitlshift,
|
||||
Function::Bitor,
|
||||
Function::Bitrshift,
|
||||
Function::Bitxor,
|
||||
Function::Complex,
|
||||
Function::Imabs,
|
||||
Function::Imaginary,
|
||||
Function::Imargument,
|
||||
Function::Imconjugate,
|
||||
Function::Imcos,
|
||||
Function::Imcosh,
|
||||
Function::Imcot,
|
||||
Function::Imcsc,
|
||||
Function::Imcsch,
|
||||
Function::Imdiv,
|
||||
Function::Imexp,
|
||||
Function::Imln,
|
||||
Function::Imlog10,
|
||||
Function::Imlog2,
|
||||
Function::Impower,
|
||||
Function::Improduct,
|
||||
Function::Imreal,
|
||||
Function::Imsec,
|
||||
Function::Imsech,
|
||||
Function::Imsin,
|
||||
Function::Imsinh,
|
||||
Function::Imsqrt,
|
||||
Function::Imsub,
|
||||
Function::Imsum,
|
||||
Function::Imtan,
|
||||
Function::Convert,
|
||||
Function::Delta,
|
||||
Function::Gestep,
|
||||
Function::Subtotal,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Function {
|
||||
/// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`.
|
||||
pub fn to_xlsx_string(&self) -> String {
|
||||
@@ -708,7 +909,23 @@ impl fmt::Display for Function {
|
||||
}
|
||||
}
|
||||
|
||||
/// Documentation for one function
|
||||
pub struct Documentation {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Produces documentation for all implemented functions
|
||||
pub fn documentation() -> Vec<Documentation> {
|
||||
let mut doc = Vec::new();
|
||||
for function in Function::into_iter() {
|
||||
doc.push(Documentation {
|
||||
name: function.to_string(),
|
||||
});
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
pub(crate) fn evaluate_function(
|
||||
&mut self,
|
||||
kind: &Function,
|
||||
@@ -928,3 +1145,66 @@ impl Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
};
|
||||
|
||||
use crate::functions::Function;
|
||||
|
||||
#[test]
|
||||
fn function_iterator() {
|
||||
// This checks that the number of functions in the enum is the same
|
||||
// as the number of functions in the Iterator.
|
||||
|
||||
// This is tricky. In Rust we cannot loop over all the members of an enum.
|
||||
// There are alternatives like using an external crate like strum.
|
||||
// But I am not in the mood for that.
|
||||
|
||||
// What we do here is read this file , extract the functions in the enum
|
||||
// and check they are the same as in the iterator
|
||||
let file = File::open("src/functions/mod.rs").unwrap();
|
||||
let reader = BufReader::new(file);
|
||||
let mut start = false;
|
||||
let mut list = Vec::new();
|
||||
|
||||
for line in reader.lines() {
|
||||
let text = line.unwrap();
|
||||
let text = text.trim().trim_end_matches(',');
|
||||
if text == "pub enum Function {" {
|
||||
start = true;
|
||||
continue;
|
||||
}
|
||||
if start {
|
||||
if text == "}" {
|
||||
break;
|
||||
}
|
||||
if text.starts_with("//") {
|
||||
// skip comments
|
||||
continue;
|
||||
}
|
||||
if text.is_empty() {
|
||||
// skip empty lines
|
||||
continue;
|
||||
}
|
||||
list.push(text.to_owned());
|
||||
}
|
||||
}
|
||||
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
||||
let iter_list = Function::into_iter()
|
||||
.map(|f| format!("{}", f).replace('.', ""))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let len = iter_list.len();
|
||||
|
||||
assert_eq!(list.len(), len);
|
||||
// We still need to check there are no duplicates. This will fail if a function in iter_list
|
||||
// is included twice and one is missing
|
||||
for function in list {
|
||||
assert!(iter_list.contains(&function.to_uppercase()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
use crate::model::Model;
|
||||
|
||||
// The gc (Garbage Collector) cleans up leftover elements in the workbook:
|
||||
// * Strings that are no longe reachable
|
||||
// * Styles that are no longer reachable
|
||||
// * ...
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn gc(&mut self) -> Result<(), String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
1
base/src/language/language.bin
Normal file
@@ -0,0 +1 @@
|
||||
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!
|
||||
@@ -1,20 +1,17 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Booleans {
|
||||
#[serde(rename = "true")]
|
||||
pub true_value: String,
|
||||
#[serde(rename = "false")]
|
||||
pub false_value: String,
|
||||
pub r#true: String,
|
||||
pub r#false: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Errors {
|
||||
#[serde(rename = "ref")]
|
||||
pub ref_value: String,
|
||||
pub r#ref: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub div: String,
|
||||
@@ -28,14 +25,14 @@ pub struct Errors {
|
||||
pub null: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Language {
|
||||
pub booleans: Booleans,
|
||||
pub errors: Errors,
|
||||
}
|
||||
|
||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
|
||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
||||
});
|
||||
|
||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"}
|
||||
//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc" }
|
||||
//! ```
|
||||
//!
|
||||
//! <small> until version 0.5.0 you should use the git dependencies as stated </small>
|
||||
@@ -31,29 +31,30 @@ pub mod expressions;
|
||||
pub mod formatter;
|
||||
pub mod language;
|
||||
pub mod locale;
|
||||
pub mod model;
|
||||
pub mod new_empty;
|
||||
pub mod number_format;
|
||||
pub mod types;
|
||||
pub mod worksheet;
|
||||
|
||||
mod functions;
|
||||
|
||||
mod actions;
|
||||
mod cast;
|
||||
mod constants;
|
||||
mod styles;
|
||||
|
||||
mod diffs;
|
||||
mod functions;
|
||||
mod implicit_intersection;
|
||||
|
||||
mod model;
|
||||
mod styles;
|
||||
mod units;
|
||||
mod user_model;
|
||||
mod utils;
|
||||
mod workbook;
|
||||
mod garbage_collector;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock_time;
|
||||
|
||||
pub use model::get_milliseconds_since_epoch;
|
||||
pub use model::Model;
|
||||
pub use user_model::UserModel;
|
||||
|
||||
BIN
base/src/locale/locales.bin
Normal file
@@ -1,32 +1,29 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Locale {
|
||||
pub dates: Dates,
|
||||
pub numbers: NumbersProperties,
|
||||
pub currency: Currency,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Currency {
|
||||
pub iso: String,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct NumbersProperties {
|
||||
#[serde(rename = "symbols-numberSystem-latn")]
|
||||
pub symbols: NumbersSymbols,
|
||||
#[serde(rename = "decimalFormats-numberSystem-latn")]
|
||||
pub decimal_formats: DecimalFormats,
|
||||
#[serde(rename = "currencyFormats-numberSystem-latn")]
|
||||
pub currency_formats: CurrencyFormats,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Dates {
|
||||
pub day_names: Vec<String>,
|
||||
pub day_names_short: Vec<String>,
|
||||
@@ -35,8 +32,7 @@ pub struct Dates {
|
||||
pub months_letter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct NumbersSymbols {
|
||||
pub decimal: String,
|
||||
pub group: String,
|
||||
@@ -54,40 +50,26 @@ pub struct NumbersSymbols {
|
||||
}
|
||||
|
||||
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct CurrencyFormats {
|
||||
pub standard: String,
|
||||
#[serde(rename = "standard-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub standard_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "standard-noCurrency")]
|
||||
pub standard_no_currency: String,
|
||||
pub accounting: String,
|
||||
#[serde(rename = "accounting-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accounting_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "accounting-noCurrency")]
|
||||
pub accounting_no_currency: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct DecimalFormats {
|
||||
pub standard: String,
|
||||
}
|
||||
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
||||
});
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> =
|
||||
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
|
||||
|
||||
pub fn get_locale(_id: &str) -> Result<&Locale, String> {
|
||||
pub fn get_locale(id: &str) -> Result<&Locale, String> {
|
||||
// TODO: pass the locale once we implement locales in Rust
|
||||
let locale = LOCALES.get("en").ok_or("Invalid locale")?;
|
||||
Ok(locale)
|
||||
}
|
||||
|
||||
// TODO: Remove this function one we implement locales properly
|
||||
pub fn get_locale_fix(id: &str) -> Result<&Locale, String> {
|
||||
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
||||
Ok(locale)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::cell::RefCell;
|
||||
// 8 November 2022 12:13 Berlin time
|
||||
|
||||
thread_local! {
|
||||
static MOCK_TIME: RefCell<i64> = RefCell::new(1667906008578);
|
||||
static MOCK_TIME: RefCell<i64> = const { RefCell::new(1667906008578) };
|
||||
}
|
||||
|
||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
@@ -20,3 +20,18 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
pub fn set_mock_time(time: i64) {
|
||||
MOCK_TIME.with(|cell| *cell.borrow_mut() = time);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::mock_time::MOCK_TIME;
|
||||
|
||||
use super::get_milliseconds_since_epoch;
|
||||
|
||||
#[test]
|
||||
fn mock_time() {
|
||||
let t = get_milliseconds_since_epoch();
|
||||
assert_eq!(t, 1667906008578);
|
||||
|
||||
MOCK_TIME.with_borrow(|v| assert_eq!(*v, 1667906008578));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! # Model
|
||||
//!
|
||||
//! Note that sheets are 0-indexed and rows and columns are 1-indexed.
|
||||
//!
|
||||
//! IronCalc is row first. A cell is referenced by (`sheet`, `row`, `column`)
|
||||
//!
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::vec::Vec;
|
||||
|
||||
@@ -47,7 +38,11 @@ use chrono_tz::Tz;
|
||||
#[cfg(test)]
|
||||
pub use crate::mock_time::get_milliseconds_since_epoch;
|
||||
|
||||
/// wasm implementation for time
|
||||
/// Number of milliseconds since January 1, 1970
|
||||
/// Used by time and date functions. It takes the value from the environment:
|
||||
/// * The Operative System
|
||||
/// * The JavaScript environment
|
||||
/// * Or mocked for tests
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
@@ -58,6 +53,11 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
.as_millis() as i64
|
||||
}
|
||||
|
||||
/// Number of milliseconds since January 1, 1970
|
||||
/// Used by time and date functions. It takes the value from the environment:
|
||||
/// * The Operative System
|
||||
/// * The JavaScript environment
|
||||
/// * Or mocked for tests
|
||||
#[cfg(not(test))]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
@@ -67,7 +67,7 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
|
||||
/// A cell might be evaluated or being evaluated
|
||||
#[derive(Clone)]
|
||||
pub enum CellState {
|
||||
pub(crate) enum CellState {
|
||||
/// The cell has already been evaluated
|
||||
Evaluated,
|
||||
/// The cell is being evaluated
|
||||
@@ -75,7 +75,7 @@ pub enum CellState {
|
||||
}
|
||||
|
||||
/// A parsed formula for a defined name
|
||||
pub enum ParsedDefinedName {
|
||||
pub(crate) enum ParsedDefinedName {
|
||||
/// CellReference (`=C4`)
|
||||
CellReference(CellReferenceIndex),
|
||||
/// A Range (`=C4:D6`)
|
||||
@@ -105,19 +105,21 @@ pub struct Model {
|
||||
/// A list of parsed formulas
|
||||
pub parsed_formulas: Vec<Vec<Node>>,
|
||||
/// A list of parsed defined names
|
||||
pub parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
||||
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
||||
/// An optimization to lookup strings faster
|
||||
pub shared_strings: HashMap<String, usize>,
|
||||
pub(crate) shared_strings: HashMap<String, usize>,
|
||||
/// An instance of the parser
|
||||
pub parser: Parser,
|
||||
pub(crate) parser: Parser,
|
||||
/// The list of cells with formulas that are evaluated of being evaluated
|
||||
pub cells: HashMap<(u32, i32, i32), CellState>,
|
||||
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
||||
/// The locale of the model
|
||||
pub locale: Locale,
|
||||
pub(crate) locale: Locale,
|
||||
/// Tha language used
|
||||
pub language: Language,
|
||||
pub(crate) language: Language,
|
||||
/// The timezone used to evaluate the model
|
||||
pub tz: Tz,
|
||||
pub(crate) tz: Tz,
|
||||
/// The view id. A view consist of a selected sheet and ranges.
|
||||
pub(crate) view_id: u32,
|
||||
}
|
||||
|
||||
// FIXME: Maybe this should be the same as CellReference
|
||||
@@ -659,7 +661,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// assert_eq!(model.workbook.worksheet(0)?.color, None);
|
||||
@@ -681,6 +683,13 @@ impl Model {
|
||||
Err(format!("Invalid color: {}", color))
|
||||
}
|
||||
|
||||
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
|
||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
worksheet.show_grid_lines = show_grid_lines;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||
use Cell::*;
|
||||
match cell {
|
||||
@@ -726,7 +735,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, true);
|
||||
@@ -798,17 +807,17 @@ impl Model {
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a model from a String representation of a workbook
|
||||
/// Returns a model from an internal binary representation of a workbook
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::cell::CellValue;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
||||
/// let model2 = Model::from_json(&model.to_json_str())?;
|
||||
/// let model2 = Model::from_bytes(&model.to_bytes())?;
|
||||
/// assert_eq!(
|
||||
/// model2.get_cell_value_by_index(0, 1, 1),
|
||||
/// Ok(CellValue::String("Stella!".to_string()))
|
||||
@@ -816,9 +825,12 @@ impl Model {
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from_json(s: &str) -> Result<Model, String> {
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::to_bytes]
|
||||
pub fn from_bytes(s: &[u8]) -> Result<Model, String> {
|
||||
let workbook: Workbook =
|
||||
serde_json::from_str(s).map_err(|_| "Error parsing workbook".to_string())?;
|
||||
bitcode::decode(s).map_err(|e| format!("Error parsing workbook: {e}"))?;
|
||||
Model::from_workbook(workbook)
|
||||
}
|
||||
|
||||
@@ -827,7 +839,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::cell::CellValue;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
@@ -883,6 +895,7 @@ impl Model {
|
||||
language,
|
||||
locale,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
|
||||
model.parse_formulas();
|
||||
@@ -896,7 +909,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
@@ -965,7 +978,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex};
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
@@ -1036,7 +1049,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
@@ -1083,7 +1096,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
@@ -1138,13 +1151,13 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
/// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string());
|
||||
/// model.evaluate();
|
||||
/// let result = model.cell_formula(sheet, row, column)?;
|
||||
/// let result = model.get_cell_formula(sheet, row, column)?;
|
||||
/// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string()));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
@@ -1152,24 +1165,33 @@ impl Model {
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::get_cell_content()]
|
||||
pub fn cell_formula(
|
||||
pub fn get_cell_formula(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<Option<String>, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
Ok(worksheet.cell(row, column).and_then(|cell| {
|
||||
cell.get_formula().map(|formula_index| {
|
||||
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
||||
let cell_ref = CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
format!("={}", to_string(formula, &cell_ref))
|
||||
})
|
||||
}))
|
||||
match worksheet.cell(row, column) {
|
||||
Some(cell) => match cell.get_formula() {
|
||||
Some(formula_index) => {
|
||||
let formula = &self
|
||||
.parsed_formulas
|
||||
.get(sheet as usize)
|
||||
.ok_or("missing sheet")?
|
||||
.get(formula_index as usize)
|
||||
.ok_or("missing formula")?;
|
||||
let cell_ref = CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
Ok(Some(format!("={}", to_string(formula, &cell_ref))))
|
||||
}
|
||||
None => Ok(None),
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the value of a cell with some text
|
||||
@@ -1178,7 +1200,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
@@ -1221,7 +1243,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
@@ -1258,7 +1280,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
@@ -1296,7 +1318,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
@@ -1348,7 +1370,7 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # use ironcalc_base::cell::CellValue;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
@@ -1358,7 +1380,7 @@ impl Model {
|
||||
/// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string());
|
||||
/// model.evaluate();
|
||||
/// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0)));
|
||||
/// assert_eq!(model.formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
|
||||
/// assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
@@ -1529,7 +1551,7 @@ impl Model {
|
||||
/// Returns the cell value for (`sheet`, `row`, `column`)
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::formatted_cell_value()]
|
||||
/// * [Model::get_formatted_cell_value()]
|
||||
pub fn get_cell_value_by_index(
|
||||
&self,
|
||||
sheet_index: u32,
|
||||
@@ -1555,35 +1577,42 @@ impl Model {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::model::Model;
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
/// model.set_user_input(sheet, row, column, "=1/3".to_string());
|
||||
/// model.evaluate();
|
||||
/// let result = model.formatted_cell_value(sheet, row, column)?;
|
||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
||||
/// assert_eq!(result, "0.333333333".to_string());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn formatted_cell_value(
|
||||
pub fn get_formatted_cell_value(
|
||||
&self,
|
||||
sheet_index: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<String, String> {
|
||||
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
|
||||
let cell = self
|
||||
.workbook
|
||||
.worksheet(sheet_index)?
|
||||
.cell(row, column)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let formatted_value =
|
||||
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
|
||||
format_number(value, &format, &self.locale).text
|
||||
});
|
||||
Ok(formatted_value)
|
||||
match self.workbook.worksheet(sheet_index)?.cell(row, column) {
|
||||
Some(cell) => {
|
||||
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
|
||||
let formatted_value =
|
||||
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
|
||||
format_number(value, &format, &self.locale).text
|
||||
});
|
||||
Ok(formatted_value)
|
||||
}
|
||||
None => Ok("".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the typeof a cell
|
||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
|
||||
Ok(match self.workbook.worksheet(sheet)?.cell(row, column) {
|
||||
Some(c) => c.get_type(),
|
||||
None => CellType::Number,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a string with the cell content. If there is a formula returns the formula
|
||||
@@ -1647,15 +1676,53 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets cell to empty. Can be used to delete value without affecting style.
|
||||
pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
worksheet.set_cell_empty(row, column);
|
||||
/// Removes the content of the cell but leaves the style.
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::cell_clear_all()]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
/// model.set_user_input(sheet, row, column, "100$".to_string());
|
||||
/// model.cell_clear_contents(sheet, row, column);
|
||||
/// model.set_user_input(sheet, row, column, "10".to_string());
|
||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
||||
/// assert_eq!(result, "10$".to_string());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.cell_clear_contents(row, column);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a cell by removing it from worksheet data.
|
||||
pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
/// Deletes a cell by removing it from worksheet data. All content and style is removed.
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::cell_clear_contents()]
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ironcalc_base::Model;
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||
/// let (sheet, row, column) = (0, 1, 1);
|
||||
/// model.set_user_input(sheet, row, column, "100$".to_string());
|
||||
/// model.cell_clear_all(sheet, row, column);
|
||||
/// model.set_user_input(sheet, row, column, "10".to_string());
|
||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
||||
/// assert_eq!(result, "10".to_string());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
|
||||
let sheet_data = &mut worksheet.sheet_data;
|
||||
@@ -1682,9 +1749,8 @@ impl Model {
|
||||
if r.r == row {
|
||||
if r.custom_format {
|
||||
return r.s;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cols = &self.workbook.worksheets[sheet as usize].cols;
|
||||
@@ -1707,19 +1773,30 @@ impl Model {
|
||||
.get_style(self.get_cell_style_index(sheet, row, column))
|
||||
}
|
||||
|
||||
/// Returns a JSON string of the workbook
|
||||
pub fn to_json_str(&self) -> String {
|
||||
match serde_json::to_string(&self.workbook) {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
// TODO, is this branch possible at all?
|
||||
json!({"error": "Error stringifying workbook"}).to_string()
|
||||
}
|
||||
}
|
||||
/// Returns an internal binary representation of the workbook
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::from_bytes]
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
bitcode::encode(&self.workbook)
|
||||
}
|
||||
|
||||
/// Returns data about the worksheets
|
||||
pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
|
||||
self.workbook
|
||||
.worksheets
|
||||
.iter()
|
||||
.map(|worksheet| SheetProperties {
|
||||
name: worksheet.get_name(),
|
||||
state: worksheet.state.to_string(),
|
||||
color: worksheet.color.clone(),
|
||||
sheet_id: worksheet.sheet_id,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns markup representation of the given `sheet`.
|
||||
pub fn sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
let dimension = worksheet.dimension();
|
||||
|
||||
@@ -1729,9 +1806,9 @@ impl Model {
|
||||
let mut row_markup: Vec<String> = Vec::new();
|
||||
|
||||
for column in 1..(dimension.max_column + 1) {
|
||||
let mut cell_markup = match self.cell_formula(sheet, row, column)? {
|
||||
let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
|
||||
Some(formula) => formula,
|
||||
None => self.formatted_cell_value(sheet, row, column)?,
|
||||
None => self.get_formatted_cell_value(sheet, row, column)?,
|
||||
};
|
||||
let style = self.get_style_for_cell(sheet, row, column);
|
||||
if style.font.b {
|
||||
@@ -1770,7 +1847,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Returns the number of frozen rows in `sheet`
|
||||
pub fn get_frozen_rows(&self, sheet: u32) -> Result<i32, String> {
|
||||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
Ok(worksheet.frozen_rows)
|
||||
} else {
|
||||
@@ -1779,7 +1856,7 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Return the number of frozen columns in `sheet`
|
||||
pub fn get_frozen_columns(&self, sheet: u32) -> Result<i32, String> {
|
||||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
|
||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||
Ok(worksheet.frozen_columns)
|
||||
} else {
|
||||
@@ -1820,6 +1897,34 @@ impl Model {
|
||||
Err("Invalid sheet".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of a column
|
||||
#[inline]
|
||||
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
|
||||
self.workbook.worksheet(sheet)?.get_column_width(column)
|
||||
}
|
||||
|
||||
/// Sets the width of a column
|
||||
#[inline]
|
||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_column_width(column, width)
|
||||
}
|
||||
|
||||
/// Returns the height of a row
|
||||
#[inline]
|
||||
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
|
||||
self.workbook.worksheet(sheet)?.row_height(row)
|
||||
}
|
||||
|
||||
/// Sets the height of a row
|
||||
#[inline]
|
||||
pub fn set_row_height(&mut self, sheet: u32, column: i32, height: f64) -> Result<(), String> {
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_row_height(column, height)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::DateTime;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -6,14 +6,18 @@ use crate::{
|
||||
calc_result::Range,
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::stringify::{rename_sheet_in_node, to_rc_format},
|
||||
parser::Parser,
|
||||
parser::{
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
Parser,
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
@@ -33,7 +37,20 @@ fn is_valid_sheet_name(name: &str) -> bool {
|
||||
|
||||
impl Model {
|
||||
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
|
||||
let mut views = HashMap::new();
|
||||
for id in view_ids {
|
||||
views.insert(
|
||||
**id,
|
||||
WorksheetView {
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
Worksheet {
|
||||
cols: vec![],
|
||||
rows: vec![],
|
||||
@@ -48,6 +65,8 @@ impl Model {
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
show_grid_lines: true,
|
||||
views,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +141,8 @@ impl Model {
|
||||
self.parsed_defined_names = parsed_defined_names;
|
||||
}
|
||||
|
||||
// Reparses all formulas and defined names
|
||||
fn reset_parsed_structures(&mut self) {
|
||||
/// Reparses all formulas and defined names
|
||||
pub(crate) fn reset_parsed_structures(&mut self) {
|
||||
self.parser
|
||||
.set_worksheets(self.workbook.get_worksheet_names());
|
||||
self.parsed_formulas = vec![];
|
||||
@@ -134,10 +153,10 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Adds a sheet with a automatically generated name
|
||||
pub fn new_sheet(&mut self) {
|
||||
pub fn new_sheet(&mut self) -> (String, u32) {
|
||||
// First we find a name
|
||||
|
||||
// TODO: When/if we support i18n the name could depend on the locale
|
||||
// TODO: The name should depend on the locale
|
||||
let base_name = "Sheet";
|
||||
let base_name_uppercase = base_name.to_uppercase();
|
||||
let mut index = 1;
|
||||
@@ -153,9 +172,11 @@ impl Model {
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
|
||||
self.workbook.worksheets.push(worksheet);
|
||||
self.reset_parsed_structures();
|
||||
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
|
||||
}
|
||||
|
||||
/// Inserts a sheet with a particular index
|
||||
@@ -183,7 +204,8 @@ impl Model {
|
||||
Some(id) => id,
|
||||
None => self.get_new_sheet_id(),
|
||||
};
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
|
||||
if sheet_index as usize > self.workbook.worksheets.len() {
|
||||
return Err("Sheet index out of range".to_string());
|
||||
}
|
||||
@@ -223,10 +245,10 @@ impl Model {
|
||||
new_name: &str,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_sheet_name(new_name) {
|
||||
return Err(format!("Invalid name for a sheet: '{}'", new_name));
|
||||
return Err(format!("Invalid name for a sheet: '{}'.", new_name));
|
||||
}
|
||||
if self.get_sheet_index_by_name(new_name).is_some() {
|
||||
return Err(format!("Sheet already exists: '{}'", new_name));
|
||||
return Err(format!("Sheet already exists: '{}'.", new_name));
|
||||
}
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
let sheet_count = worksheets.len() as u32;
|
||||
@@ -270,7 +292,7 @@ impl Model {
|
||||
if sheet_count == 1 {
|
||||
return Err("Cannot delete only sheet".to_string());
|
||||
};
|
||||
if sheet_index > sheet_count {
|
||||
if sheet_index >= sheet_count {
|
||||
return Err("Sheet index too large".to_string());
|
||||
}
|
||||
self.workbook.worksheets.remove(sheet_index as usize);
|
||||
@@ -323,18 +345,21 @@ impl Model {
|
||||
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
||||
Some(s) => s,
|
||||
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
||||
};
|
||||
// "2020-08-06T21:20:53Z
|
||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
let mut views = HashMap::new();
|
||||
views.insert(0, WorkbookView { sheet: 0 });
|
||||
|
||||
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
||||
let workbook = Workbook {
|
||||
shared_strings: vec![],
|
||||
defined_names: vec![],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
|
||||
styles: Default::default(),
|
||||
name: name.to_string(),
|
||||
settings: WorkbookSettings {
|
||||
@@ -350,6 +375,7 @@ impl Model {
|
||||
last_modified: now,
|
||||
},
|
||||
tables: HashMap::new(),
|
||||
views,
|
||||
};
|
||||
let parsed_formulas = Vec::new();
|
||||
let worksheets = &workbook.worksheets;
|
||||
@@ -370,6 +396,7 @@ impl Model {
|
||||
locale,
|
||||
language,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
model.parse_formulas();
|
||||
Ok(model)
|
||||
|
||||
@@ -76,10 +76,16 @@ fn fn_imconjugate() {
|
||||
fn fn_imcos() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOS("4+3i")"#);
|
||||
// In macos non intel this is "-6.58066304055116+7.58155274274655i"
|
||||
model._set("A2", r#"=COMPLEX(-6.58066304055116, 7.58155274274654)"#);
|
||||
model._set("A3", r#"=IMABS(IMSUB(A1, A2)) < G1"#);
|
||||
|
||||
// small number
|
||||
model._set("G1", "0.0000001");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i");
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod test_actions;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
mod test_cell_clear_contents;
|
||||
mod test_circular_references;
|
||||
mod test_column_width;
|
||||
mod test_criteria;
|
||||
@@ -28,9 +29,8 @@ mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_math;
|
||||
mod test_metadata;
|
||||
mod test_model_delete_cell;
|
||||
mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_model_set_cell_empty;
|
||||
mod test_move_formula;
|
||||
mod test_quote_prefix;
|
||||
mod test_set_user_input;
|
||||
@@ -53,4 +53,5 @@ mod test_frozen_rows_and_columns;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
mod test_gc;
|
||||
mod test_types;
|
||||
mod user_model;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use crate::constants::LAST_COLUMN;
|
||||
use crate::model::Model;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::Col;
|
||||
|
||||
#[test]
|
||||
fn test_insert_columns() {
|
||||
@@ -195,6 +196,250 @@ fn test_delete_columns() {
|
||||
assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_column_width() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, column) = (0, 5);
|
||||
let normal_width = model.get_column_width(sheet, column).unwrap();
|
||||
// Set the width of one column to 5 times the normal width
|
||||
assert!(model
|
||||
.set_column_width(sheet, column, normal_width * 5.0)
|
||||
.is_ok());
|
||||
|
||||
// delete it
|
||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
||||
|
||||
// all the columns around have the expected width
|
||||
assert_eq!(
|
||||
model.get_column_width(sheet, column - 1).unwrap(),
|
||||
normal_width
|
||||
);
|
||||
assert_eq!(model.get_column_width(sheet, column).unwrap(), normal_width);
|
||||
assert_eq!(
|
||||
model.get_column_width(sheet, column + 1).unwrap(),
|
||||
normal_width
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// We set the style of columns 4 to 7 and delete column 4
|
||||
// We check that columns 4 to 6 have the new style
|
||||
fn test_delete_first_column_width() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 4,
|
||||
max: 7,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 4);
|
||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 4,
|
||||
max: 6,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Delete the last column in the range
|
||||
fn test_delete_last_column_width() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 4,
|
||||
max: 7,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 7);
|
||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 4,
|
||||
max: 6,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Deletes columns at the end
|
||||
fn test_delete_last_few_columns_width() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 4,
|
||||
max: 17,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 13);
|
||||
assert!(model.delete_columns(sheet, column, 10).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 4,
|
||||
max: 12,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_columns_non_overlapping_left() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 10,
|
||||
max: 17,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 3);
|
||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 6,
|
||||
max: 13,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_columns_overlapping_left() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 10,
|
||||
max: 20,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 8);
|
||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 8,
|
||||
max: 16,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_columns_non_overlapping_right() {
|
||||
let mut model = new_empty_model();
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 10,
|
||||
max: 17,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
let (sheet, column) = (0, 23);
|
||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 10,
|
||||
max: 17,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// deletes some columns in the middle of the range
|
||||
fn test_delete_middle_column_width() {
|
||||
let mut model = new_empty_model();
|
||||
// styled columns [4, 17]
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 4,
|
||||
max: 17,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
|
||||
// deletes columns 10, 11, 12
|
||||
let (sheet, column) = (0, 10);
|
||||
assert!(model.delete_columns(sheet, column, 3).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 1);
|
||||
assert_eq!(
|
||||
cols[0],
|
||||
Col {
|
||||
min: 4,
|
||||
max: 14,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// the range is inside the deleted columns
|
||||
fn delete_range_in_columns() {
|
||||
let mut model = new_empty_model();
|
||||
// styled columns [6, 10]
|
||||
model.workbook.worksheets[0].cols = vec![Col {
|
||||
min: 6,
|
||||
max: 10,
|
||||
width: 300.0,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
}];
|
||||
|
||||
// deletes columns [4, 17]
|
||||
let (sheet, column) = (0, 4);
|
||||
assert!(model.delete_columns(sheet, column, 8).is_ok());
|
||||
let cols = &model.workbook.worksheets[0].cols;
|
||||
assert_eq!(cols.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_columns_error() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, column) = (0, 5);
|
||||
assert!(model.delete_columns(sheet, column, -1).is_err());
|
||||
assert!(model.delete_columns(sheet, column, 0).is_err());
|
||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_rows() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_set_cell_empty_non_existing_sheet() {
|
||||
fn test_cell_clear_contents_non_existing_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.set_cell_empty(13, 1, 1),
|
||||
model.cell_clear_contents(13, 1, 1),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cell_empty_unset_cell() {
|
||||
fn test_cell_clear_contents_unset_cell() {
|
||||
let mut model = new_empty_model();
|
||||
model.set_cell_empty(0, 1, 1).unwrap();
|
||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true));
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cell_empty_with_value() {
|
||||
fn test_cell_clear_contents_with_value() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "hello");
|
||||
model.evaluate();
|
||||
@@ -28,7 +28,7 @@ fn test_set_cell_empty_with_value() {
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||
|
||||
model.set_cell_empty(0, 1, 1).unwrap();
|
||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||
@@ -36,7 +36,7 @@ fn test_set_cell_empty_with_value() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cell_empty_referenced_elsewhere() {
|
||||
fn test_cell_clear_contents_referenced_elsewhere() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "35");
|
||||
model._set("A2", "=2*A1");
|
||||
@@ -47,7 +47,7 @@ fn test_set_cell_empty_referenced_elsewhere() {
|
||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
||||
|
||||
model.set_cell_empty(0, 1, 1).unwrap();
|
||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||
@@ -23,9 +23,9 @@ fn test_column_width() {
|
||||
.unwrap();
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 3);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!((worksheet.column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 2), 6);
|
||||
}
|
||||
|
||||
@@ -48,9 +48,11 @@ fn test_column_width_lower_edge() {
|
||||
.unwrap();
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!((worksheet.column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!(
|
||||
(worksheet.get_column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
||||
);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 5), 1);
|
||||
}
|
||||
|
||||
@@ -74,9 +76,9 @@ fn test_column_width_higher_edge() {
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!(
|
||||
(worksheet.column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
||||
(worksheet.get_column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
||||
);
|
||||
assert!((worksheet.column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 16), 1);
|
||||
}
|
||||
|
||||
@@ -8,34 +8,37 @@ use crate::{
|
||||
#[test]
|
||||
fn test_empty_model() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_rows_count(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
||||
|
||||
let e = model.set_frozen_rows(0, 3);
|
||||
assert!(e.is_ok());
|
||||
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_rows_count(0), Ok(3));
|
||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
||||
|
||||
let e = model.set_frozen_columns(0, 53);
|
||||
assert!(e.is_ok());
|
||||
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||
assert_eq!(model.get_frozen_columns(0), Ok(53));
|
||||
assert_eq!(model.get_frozen_rows_count(0), Ok(3));
|
||||
assert_eq!(model.get_frozen_columns_count(0), Ok(53));
|
||||
|
||||
// Set them back to zero
|
||||
let e = model.set_frozen_rows(0, 0);
|
||||
assert!(e.is_ok());
|
||||
let e = model.set_frozen_columns(0, 0);
|
||||
assert!(e.is_ok());
|
||||
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_rows_count(0), Ok(0));
|
||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(model.get_frozen_rows(1), Err("Invalid sheet".to_string()));
|
||||
assert_eq!(
|
||||
model.get_frozen_columns(3),
|
||||
model.get_frozen_rows_count(1),
|
||||
Err("Invalid sheet".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_frozen_columns_count(3),
|
||||
Err("Invalid sheet".to_string())
|
||||
);
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_empty_model() {
|
||||
let mut model = new_empty_model();
|
||||
// set a string
|
||||
model._set("A1", "Hello");
|
||||
assert_eq!(model.shared_strings.len(), 1);
|
||||
// calling the gc doesn't do anything
|
||||
model.gc().unwrap();
|
||||
assert_eq!(model.shared_strings.len(), 1);
|
||||
|
||||
// If we delete the cell the string is still in the list
|
||||
model.delete_cell(0, 1, 1).unwrap();
|
||||
assert_eq!(model.shared_strings.len(), 1);
|
||||
|
||||
// after the gc the string is no longer present
|
||||
model.gc().unwrap();
|
||||
assert_eq!(model.shared_strings.len(), 0);
|
||||
}
|
||||
@@ -411,11 +411,11 @@ fn test_get_formatted_cell_value() {
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "foobar");
|
||||
assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "TRUE");
|
||||
assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), "");
|
||||
assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "123.456");
|
||||
assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "$123.46");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "foobar");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "TRUE");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "123.456");
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "$123.46");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -426,20 +426,20 @@ fn test_cell_formula() {
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 1, 1), // A1
|
||||
model.get_cell_formula(0, 1, 1), // A1
|
||||
Ok(Some("=1+2+3".to_string())),
|
||||
);
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 2, 1), // A2
|
||||
model.get_cell_formula(0, 2, 1), // A2
|
||||
Ok(None),
|
||||
);
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 3, 1), // A3 - empty cell
|
||||
model.get_cell_formula(0, 3, 1), // A3 - empty cell
|
||||
Ok(None),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.cell_formula(42, 1, 1),
|
||||
model.get_cell_formula(42, 1, 1),
|
||||
Err("Invalid sheet index".to_string()),
|
||||
);
|
||||
}
|
||||
@@ -453,16 +453,16 @@ fn test_xlfn() {
|
||||
model.evaluate();
|
||||
// Only modern formulas strip the '_xlfn.'
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 1, 1).unwrap(),
|
||||
model.get_cell_formula(0, 1, 1).unwrap(),
|
||||
Some("=_xlfn.SIN(1)".to_string())
|
||||
);
|
||||
// unknown formulas keep the '_xlfn.' prefix
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 2, 1).unwrap(),
|
||||
model.get_cell_formula(0, 2, 1).unwrap(),
|
||||
Some("=_xlfn.SINY(1)".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 3, 1).unwrap(),
|
||||
model.get_cell_formula(0, 3, 1).unwrap(),
|
||||
Some("=CONCAT(3,4)".to_string())
|
||||
);
|
||||
}
|
||||
@@ -474,11 +474,11 @@ fn test_letter_case() {
|
||||
model._set("A2", "=sIn(2)");
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 1, 1).unwrap(),
|
||||
model.get_cell_formula(0, 1, 1).unwrap(),
|
||||
Some("=SIN(1)".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.cell_formula(0, 2, 1).unwrap(),
|
||||
model.get_cell_formula(0, 2, 1).unwrap(),
|
||||
Some("=SIN(2)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_delete_cell_non_existing_sheet() {
|
||||
fn test_cell_clear_all_non_existing_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.delete_cell(13, 1, 1),
|
||||
model.cell_clear_all(13, 1, 1),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_cell_unset_cell() {
|
||||
fn test_cell_clear_all_unset_cell() {
|
||||
let mut model = new_empty_model();
|
||||
assert!(model.delete_cell(0, 1, 1).is_ok());
|
||||
assert!(model.cell_clear_all(0, 1, 1).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_cell_with_value() {
|
||||
fn test_cell_clear_all_with_value() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "hello");
|
||||
model.evaluate();
|
||||
@@ -25,7 +25,7 @@ fn test_delete_cell_with_value() {
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||
|
||||
model.delete_cell(0, 1, 1).unwrap();
|
||||
model.cell_clear_all(0, 1, 1).unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||
@@ -33,7 +33,7 @@ fn test_delete_cell_with_value() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_cell_referenced_elsewhere() {
|
||||
fn test_cell_clear_all_referenced_elsewhere() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "35");
|
||||
model._set("A2", "=2*A1");
|
||||
@@ -44,7 +44,7 @@ fn test_delete_cell_referenced_elsewhere() {
|
||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
||||
|
||||
model.delete_cell(0, 1, 1).unwrap();
|
||||
model.cell_clear_all(0, 1, 1).unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||
@@ -16,7 +16,7 @@ fn test_is_empty_cell() {
|
||||
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
||||
model.set_user_input(0, 3, 1, "Hello World".to_string());
|
||||
assert!(!model.is_empty_cell(0, 3, 1).unwrap());
|
||||
model.set_cell_empty(0, 3, 1).unwrap();
|
||||
model.cell_clear_contents(0, 3, 1).unwrap();
|
||||
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ fn test_sheet_markup() {
|
||||
model.set_cell_style(0, 4, 1, &style).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.sheet_markup(0),
|
||||
model.get_sheet_markup(0),
|
||||
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -236,3 +236,11 @@ fn test_delete_sheet_by_index() {
|
||||
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]);
|
||||
assert_eq!(model._get_text("Sheet2!A1"), "#REF!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_sheet_error() {
|
||||
let mut model = new_empty_model();
|
||||
model.new_sheet();
|
||||
assert!(model.delete_sheet(2).is_err());
|
||||
assert!(model.delete_sheet(1).is_ok());
|
||||
}
|
||||
|
||||
24
base/src/test/test_types.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
|
||||
|
||||
#[test]
|
||||
fn alignment_default() {
|
||||
let alignment = Alignment::default();
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: false
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&alignment).unwrap();
|
||||
// defaults stringifies as an empty object
|
||||
assert_eq!(s, "{}");
|
||||
|
||||
let a: Alignment = serde_json::from_str("{}").unwrap();
|
||||
|
||||
assert_eq!(a, alignment)
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{test::util::new_empty_model, types::SheetInfo};
|
||||
use crate::{test::util::new_empty_model, types::SheetProperties};
|
||||
|
||||
#[test]
|
||||
fn workbook_worksheets_info() {
|
||||
let model = new_empty_model();
|
||||
let sheets_info = model.workbook.get_worksheets_info();
|
||||
let sheets_info = model.get_worksheets_properties();
|
||||
assert_eq!(
|
||||
sheets_info[0],
|
||||
SheetInfo {
|
||||
SheetProperties {
|
||||
name: "Sheet1".to_string(),
|
||||
state: "visible".to_string(),
|
||||
sheet_id: 1,
|
||||
|
||||
@@ -39,7 +39,7 @@ fn test_worksheet_dimension_single_cell() {
|
||||
fn test_worksheet_dimension_single_cell_set_empty() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("W11", "1");
|
||||
model.set_cell_empty(0, 11, 23).unwrap();
|
||||
model.cell_clear_contents(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
@@ -55,7 +55,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
|
||||
fn test_worksheet_dimension_single_cell_deleted() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("W11", "1");
|
||||
model.delete_cell(0, 11, 23).unwrap();
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
@@ -75,7 +75,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
model._set("AA17", "1");
|
||||
model._set("G17", "1");
|
||||
model._set("B19", "1");
|
||||
model.delete_cell(0, 11, 23).unwrap();
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
|
||||
14
base/src/test/user_model/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
mod test_add_delete_sheets;
|
||||
mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_clear_cells;
|
||||
mod test_diff_queue;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_grid_lines;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
mod test_view;
|
||||
84
base/src/test/user_model/test_add_delete_sheets.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
|
||||
|
||||
#[test]
|
||||
fn add_undo_redo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet();
|
||||
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
||||
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
||||
model
|
||||
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||
.unwrap();
|
||||
model.new_sheet();
|
||||
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
||||
|
||||
model.undo().unwrap();
|
||||
model.undo().unwrap();
|
||||
|
||||
assert!(model.get_formatted_cell_value(2, 1, 1).is_err());
|
||||
|
||||
model.redo().unwrap();
|
||||
model.redo().unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
||||
|
||||
model.delete_sheet(1).unwrap();
|
||||
|
||||
assert!(!model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_sheet_color() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_sheet_color(0, "#343434").unwrap();
|
||||
let worksheets_properties = model.get_worksheets_properties();
|
||||
assert_eq!(worksheets_properties.len(), 1);
|
||||
assert_eq!(worksheets_properties[0].color, Some("#343434".to_owned()));
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_worksheets_properties()[0].color,
|
||||
Some("#343434".to_owned())
|
||||
);
|
||||
// changes the color if there is one
|
||||
model.set_sheet_color(0, "#2534FF").unwrap();
|
||||
assert_eq!(
|
||||
model.get_worksheets_properties()[0].color,
|
||||
Some("#2534FF".to_owned())
|
||||
);
|
||||
// Setting it back to none
|
||||
model.set_sheet_color(0, "").unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].color, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_sheet_propagates() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet();
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
let worksheets_properties = model2.get_worksheets_properties();
|
||||
assert_eq!(worksheets_properties.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_sheet_propagates() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.new_sheet();
|
||||
model.delete_sheet(0).unwrap();
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
let sheets_info = model2.get_worksheets_properties();
|
||||
assert_eq!(sheets_info.len(), 1);
|
||||
}
|
||||
404
base/src/test/user_model/test_autofill_columns.rs
Normal file
@@ -0,0 +1,404 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to C3
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
// B3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 2),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
// C3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 3),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_right() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
// B1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap(); // A1
|
||||
model.set_user_input(0, 1, 2, "Bethe").unwrap(); // B1
|
||||
model.set_user_input(0, 1, 3, "Gamow").unwrap(); // C1
|
||||
model.set_user_input(0, 2, 1, "=A1").unwrap(); // A2
|
||||
model.set_user_input(0, 2, 2, "=B1").unwrap(); // B2
|
||||
model.set_user_input(0, 2, 3, "=C1").unwrap(); // C2
|
||||
|
||||
// We autofill from A1:C2 to I2
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 2,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// D1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 2, 4), Ok("=D1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:C1
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let b1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let c1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&b1, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&c1, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell E1 has B1 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 1, 4), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 1, 10, "Alpher").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Bethe").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 1, 10, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 1, 13, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill left to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
model.set_user_input(0, 1, 4, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 1,
|
||||
column: 4,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: -1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16392'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW + 1,
|
||||
column: 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048583'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
399
base/src/test/user_model/test_autofill_rows.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to A5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_down() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
||||
model.set_user_input(0, 2, 2, "=A2").unwrap();
|
||||
model.set_user_input(0, 3, 2, "=A3").unwrap();
|
||||
// We autofill from A1:B3 to A9
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 2), Ok("=A4".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let a2 = Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let a3 = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&a2, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&a3, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 1), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 10, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 10, 1, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 13, 1, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 4,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 4, 1, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 4,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: -1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048584'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN + 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 2,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16391'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
91
base/src/test/user_model/test_clear_cells.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{expressions::types::Area, UserModel};
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_user_input(0, 1, 1, "100$").unwrap();
|
||||
model
|
||||
.range_clear_contents(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("100$".to_string())
|
||||
);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
|
||||
model.set_user_input(0, 1, 1, "300").unwrap();
|
||||
// clear contents keeps the formatting
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("300$".to_string())
|
||||
);
|
||||
|
||||
model
|
||||
.range_clear_all(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
model.undo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("300$".to_string())
|
||||
);
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
model.set_user_input(0, 1, 1, "400").unwrap();
|
||||
// clear contents keeps the formatting
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("400".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_empty_cell() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.range_clear_contents(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_all_empty_cell() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model
|
||||
.range_clear_all(&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
}
|
||||
161
base/src/test/user_model/test_diff_queue.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::{
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn send_queue() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
let send_queue = model1.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("Hello IronCalc!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_external_diffs_wrong_str() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
assert!(model1.apply_external_diffs("invalid".as_bytes()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_undo_redo() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
assert!(model1.undo().is_ok());
|
||||
assert!(model1.redo().is_ok());
|
||||
let send_queue = model1.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("Hello IronCalc!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_undo_redo_multiple() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
|
||||
// do a bunch of things
|
||||
model1.set_frozen_columns_count(0, 5).unwrap();
|
||||
model1.set_frozen_rows_count(0, 6).unwrap();
|
||||
model1.set_column_width(0, 7, 300.0).unwrap();
|
||||
model1.set_row_height(0, 23, 123.0).unwrap();
|
||||
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
||||
|
||||
for row in 1..5 {
|
||||
model1.set_user_input(0, row, 17, "=ROW()").unwrap();
|
||||
}
|
||||
|
||||
model1.insert_row(0, 3).unwrap();
|
||||
model1.insert_row(0, 3).unwrap();
|
||||
|
||||
// undo al of them
|
||||
while model1.can_undo() {
|
||||
model1.undo().unwrap();
|
||||
}
|
||||
|
||||
// check it is an empty model
|
||||
assert_eq!(model1.get_frozen_columns_count(0), Ok(0));
|
||||
assert_eq!(model1.get_frozen_rows_count(0), Ok(0));
|
||||
assert_eq!(model1.get_column_width(0, 7), Ok(DEFAULT_COLUMN_WIDTH));
|
||||
assert_eq!(
|
||||
model1.get_formatted_cell_value(0, 55, 55),
|
||||
Ok("".to_string())
|
||||
);
|
||||
assert_eq!(model1.get_row_height(0, 23), Ok(DEFAULT_ROW_HEIGHT));
|
||||
assert_eq!(
|
||||
model1.get_formatted_cell_value(0, 57, 55),
|
||||
Ok("".to_string())
|
||||
);
|
||||
assert_eq!(model1.get_row_height(0, 25), Ok(DEFAULT_ROW_HEIGHT));
|
||||
|
||||
// redo all of them
|
||||
while model1.can_redo() {
|
||||
model1.redo().unwrap();
|
||||
}
|
||||
|
||||
// now send all this to a new model
|
||||
let send_queue = model1.flush_send_queue();
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
// Check everything is as expected
|
||||
assert_eq!(model2.get_frozen_columns_count(0), Ok(5));
|
||||
assert_eq!(model2.get_frozen_rows_count(0), Ok(6));
|
||||
assert_eq!(model2.get_column_width(0, 7), Ok(300.0));
|
||||
// I inserted two rows
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 57, 55),
|
||||
Ok("50".to_string())
|
||||
);
|
||||
assert_eq!(model2.get_row_height(0, 25), Ok(123.0));
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 1, 17),
|
||||
Ok("1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 2, 17),
|
||||
Ok("2".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 3, 17),
|
||||
Ok("".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 4, 17),
|
||||
Ok("".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 5, 17),
|
||||
Ok("5".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 6, 17),
|
||||
Ok("6".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_sheet() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
model1.new_sheet();
|
||||
model1.set_user_input(0, 1, 1, "42").unwrap();
|
||||
model1.set_user_input(1, 1, 1, "=Sheet1!A1*2").unwrap();
|
||||
|
||||
let send_queue = model1.flush_send_queue();
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(1, 1, 1),
|
||||
Ok("84".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_diffs_handled() {
|
||||
let mut model = UserModel::from_model(new_empty_model());
|
||||
assert!(model
|
||||
.apply_external_diffs("Hello world".as_bytes())
|
||||
.is_err());
|
||||
}
|
||||
31
base/src/test/user_model/test_evaluation.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn model_evaluates_automatically() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_user_input(0, 1, 1, "=1 + 1").unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pause_resume_evaluation() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.pause_evaluation();
|
||||
model.set_user_input(0, 1, 1, "=1+1").unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("#ERROR!".to_string())
|
||||
);
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
|
||||
|
||||
model.resume_evaluation();
|
||||
model.set_user_input(0, 2, 1, "=1+4").unwrap();
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1), Ok("5".to_string()));
|
||||
}
|
||||
131
base/src/test/user_model/test_general.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::CellType;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn set_user_input_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// Wrong sheet
|
||||
assert!(model.set_user_input(1, 1, 1, "1").is_err());
|
||||
// Wrong row
|
||||
assert!(model.set_user_input(0, 0, 1, "1").is_err());
|
||||
// Wrong column
|
||||
assert!(model.set_user_input(0, 1, 0, "1").is_err());
|
||||
// row too large
|
||||
assert!(model.set_user_input(0, LAST_ROW, 1, "1").is_ok());
|
||||
assert!(model.set_user_input(0, LAST_ROW + 1, 1, "1").is_err());
|
||||
// column too large
|
||||
assert!(model.set_user_input(0, 1, LAST_COLUMN, "1").is_ok());
|
||||
assert!(model.set_user_input(0, 1, LAST_COLUMN + 1, "1").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_model_debug_message() {
|
||||
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let s = &format!("{:?}", model);
|
||||
assert_eq!(s, "UserModel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_type() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.set_user_input(0, 1, 1, "1").unwrap();
|
||||
model.set_user_input(0, 1, 2, "Wish you were here").unwrap();
|
||||
model.set_user_input(0, 1, 3, "true").unwrap();
|
||||
model.set_user_input(0, 1, 4, "=1/0").unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_type(0, 1, 1).unwrap(), CellType::Number);
|
||||
assert_eq!(model.get_cell_type(0, 1, 2).unwrap(), CellType::Text);
|
||||
assert_eq!(
|
||||
model.get_cell_type(0, 1, 3).unwrap(),
|
||||
CellType::LogicalValue
|
||||
);
|
||||
assert_eq!(model.get_cell_type(0, 1, 4).unwrap(), CellType::ErrorValue);
|
||||
|
||||
// empty cells are number type
|
||||
assert_eq!(model.get_cell_type(0, 40, 40).unwrap(), CellType::Number);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_remove_rows() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
let height = model.get_row_height(0, 5).unwrap();
|
||||
|
||||
// Insert some data in row 5 (and change the style)
|
||||
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
||||
// Change the height of the column
|
||||
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
|
||||
|
||||
// remove the row
|
||||
assert!(model.delete_row(0, 5).is_ok());
|
||||
// Row 5 has now the normal height
|
||||
assert_eq!(model.get_row_height(0, 5), Ok(height));
|
||||
// There is no value in A5
|
||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1), Ok("".to_string()));
|
||||
// Setting a value will not format it
|
||||
assert!(model.set_user_input(0, 5, 1, "125").is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("125".to_string())
|
||||
);
|
||||
|
||||
// undo twice
|
||||
assert!(model.undo().is_ok());
|
||||
assert!(model.undo().is_ok());
|
||||
|
||||
assert_eq!(model.get_row_height(0, 5), Ok(3.0 * height));
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("100$".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_remove_columns() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// column E
|
||||
let column_width = model.get_column_width(0, 5).unwrap();
|
||||
println!("{column_width}");
|
||||
|
||||
// Insert some data in row 5 (and change the style) in E1
|
||||
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
|
||||
// Change the width of the column
|
||||
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
|
||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
||||
|
||||
// remove the column
|
||||
assert!(model.delete_column(0, 5).is_ok());
|
||||
// Column 5 has now the normal width
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
|
||||
// There is no value in E5
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5), Ok("".to_string()));
|
||||
// Setting a value will not format it
|
||||
assert!(model.set_user_input(0, 1, 5, "125").is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("125".to_string())
|
||||
);
|
||||
|
||||
// undo twice (set_user_input and delete_column)
|
||||
assert!(model.undo().is_ok());
|
||||
assert!(model.undo().is_ok());
|
||||
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(3.0 * column_width));
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("100$".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_remove_cell() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let (sheet, row, column) = (0, 1, 1);
|
||||
model.set_user_input(sheet, row, column, "100$").unwrap();
|
||||
}
|
||||
42
base/src/test/user_model/test_grid_lines.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
|
||||
// default sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
// default new sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
|
||||
// wrong sheet number
|
||||
assert_eq!(
|
||||
model.get_show_grid_lines(2),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
// we can set it
|
||||
model.set_show_grid_lines(1, false).unwrap();
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(model2.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model2.get_show_grid_lines(0), Ok(true));
|
||||
}
|
||||
39
base/src/test/user_model/test_rename_sheet.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_rename() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.rename_sheet(0, "NewSheet").unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_redo() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model.rename_sheet(0, "NewSheet").unwrap();
|
||||
model.undo().unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].name, "Sheet1");
|
||||
model.redo().unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
assert_eq!(
|
||||
model.rename_sheet(0, ""),
|
||||
Err("Invalid name for a sheet: ''.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.rename_sheet(1, "Hello"),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
}
|
||||
156
base/src/test/user_model/test_row_column.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
|
||||
test::util::new_empty_model,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn simple_insert_row() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
let (sheet, column) = (0, 5);
|
||||
for row in 1..5 {
|
||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||
}
|
||||
assert!(model.insert_row(sheet, 3).is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
||||
""
|
||||
);
|
||||
|
||||
assert!(model.undo().is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
||||
"123"
|
||||
);
|
||||
assert!(model.redo().is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_insert_column() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
let (sheet, row) = (0, 5);
|
||||
for column in 1..5 {
|
||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
||||
}
|
||||
assert!(model.insert_column(sheet, 3).is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
||||
|
||||
assert!(model.undo().is_ok());
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(sheet, row, 3).unwrap(),
|
||||
"123"
|
||||
);
|
||||
assert!(model.redo().is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_delete_column() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 5, "3").unwrap();
|
||||
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
||||
model
|
||||
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_column(0, 5).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("6".to_string()));
|
||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH * 3.0));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 2, 5),
|
||||
Ok("6".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model2.get_column_width(0, 5),
|
||||
Ok(DEFAULT_COLUMN_WIDTH * 3.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_column_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.delete_column(1, 1),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.delete_column(0, 0),
|
||||
Err("Column number '0' is not valid.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.delete_column(0, LAST_COLUMN + 1),
|
||||
Err("Column number '16385' is not valid.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_delete_row() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||
|
||||
model
|
||||
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||
.unwrap();
|
||||
|
||||
model.delete_row(0, 15).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 15, 6),
|
||||
Ok("6".to_string())
|
||||
);
|
||||
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 15, 6),
|
||||
Ok("6".to_string())
|
||||
);
|
||||
assert_eq!(model2.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_delete_row_no_style() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||
model.delete_row(0, 15).unwrap();
|
||||
|
||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
||||
}
|
||||
723
base/src/test/user_model/test_styles.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
expressions::types::Area,
|
||||
types::{Alignment, BorderItem, BorderStyle, HorizontalAlignment, VerticalAlignment},
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn basic_fonts() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
assert!(!style.font.b);
|
||||
assert!(!style.font.u);
|
||||
assert!(!style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#000000".to_owned()));
|
||||
|
||||
// bold
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
// italics
|
||||
model.update_range_style(&range, "font.i", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
|
||||
// underline
|
||||
model.update_range_style(&range, "font.u", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.u);
|
||||
|
||||
// strike
|
||||
model
|
||||
.update_range_style(&range, "font.strike", "true")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.strike);
|
||||
|
||||
// color
|
||||
model
|
||||
.update_range_style(&range, "font.color", "#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
|
||||
|
||||
while model.can_undo() {
|
||||
model.undo().unwrap();
|
||||
}
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
assert!(!style.font.b);
|
||||
assert!(!style.font.u);
|
||||
assert!(!style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#000000".to_owned()));
|
||||
|
||||
while model.can_redo() {
|
||||
model.redo().unwrap();
|
||||
}
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
assert!(style.font.b);
|
||||
assert!(style.font.u);
|
||||
assert!(style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
assert!(style.font.b);
|
||||
assert!(style.font.u);
|
||||
assert!(style.font.strike);
|
||||
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_errors() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.b", "True"),
|
||||
Err("Invalid value for boolean: 'True'.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.i", "FALSE"),
|
||||
Err("Invalid value for boolean: 'FALSE'.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.bold", "true"),
|
||||
Err("Invalid style path: 'font.bold'.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.strike", ""),
|
||||
Err("Invalid value for boolean: ''.".to_string())
|
||||
);
|
||||
// There is no cast for booleans
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.b", "1"),
|
||||
Err("Invalid value for boolean: '1'.".to_string())
|
||||
);
|
||||
// colors don't work by name
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.color", "blue"),
|
||||
Err("Invalid color: 'blue'.".to_string())
|
||||
);
|
||||
// No short form
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "font.color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_fill() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
assert_eq!(style.fill.fg_color, None);
|
||||
|
||||
// bg_color
|
||||
model
|
||||
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
|
||||
.unwrap();
|
||||
model
|
||||
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_errors() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.bg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.fg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_format() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.num_fmt, "general");
|
||||
|
||||
model
|
||||
.update_range_style(&range, "num_fmt", "$#,##0.0000")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.num_fmt, "general");
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_borders() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "thin,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "thin,")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: None,
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.right", "dotted,#F1F1F2")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.right,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Dotted,
|
||||
color: Some("#F1F1F2".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.top", "double,#F1F1F3")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.top,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Double,
|
||||
color: Some("#F1F1F3".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.bottom", "medium,#F1F1F4")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.bottom,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Medium,
|
||||
color: Some("#F1F1F4".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
while model.can_undo() {
|
||||
model.undo().unwrap();
|
||||
}
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.border.left, None);
|
||||
assert_eq!(style.border.top, None);
|
||||
assert_eq!(style.border.right, None);
|
||||
assert_eq!(style.border.bottom, None);
|
||||
|
||||
while model.can_redo() {
|
||||
model.redo().unwrap();
|
||||
}
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.right,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Dotted,
|
||||
color: Some("#F1F1F2".to_owned()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.top,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Double,
|
||||
color: Some("#F1F1F3".to_owned()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.bottom,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Medium,
|
||||
color: Some("#F1F1F4".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Thin,
|
||||
color: None,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.right,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Dotted,
|
||||
color: Some("#F1F1F2".to_owned()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.top,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Double,
|
||||
color: Some("#F1F1F3".to_owned()),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
style.border.bottom,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Medium,
|
||||
color: Some("#F1F1F4".to_owned()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_alignment() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(alignment, None);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "alignment.horizontal", "center")
|
||||
.unwrap();
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::Center,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: false
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
|
||||
.unwrap();
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::CenterContinuous,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: false
|
||||
})
|
||||
);
|
||||
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&range, "alignment.vertical", "distributed")
|
||||
.unwrap();
|
||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Distributed,
|
||||
wrap_text: false
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "alignment.vertical", "justify")
|
||||
.unwrap();
|
||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Justify,
|
||||
wrap_text: false
|
||||
})
|
||||
);
|
||||
|
||||
model.update_range_style(&range, "alignment", "").unwrap();
|
||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
||||
assert_eq!(alignment, None);
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Justify,
|
||||
wrap_text: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alignment_errors() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment", "some"),
|
||||
Err("Alignment must be empty, but found: 'some'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.vertical", "justified"),
|
||||
Err("Invalid value for vertical alignment: 'justified'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
|
||||
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "alignment.vertical", "justify")
|
||||
.unwrap();
|
||||
|
||||
// Also fail if there is an alignment
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment", "some"),
|
||||
Err("Alignment must be empty, but found: 'some'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.vertical", "justified"),
|
||||
Err("Invalid value for vertical alignment: 'justified'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
|
||||
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_wrap_text() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.wrap_text", "T"),
|
||||
Err("Invalid value for boolean: 'T'.".to_string())
|
||||
);
|
||||
model
|
||||
.update_range_style(&range, "alignment.wrap_text", "true")
|
||||
.unwrap();
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: true
|
||||
})
|
||||
);
|
||||
model.undo().unwrap();
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(alignment, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Some(Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: true
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "alignment.wrap_text", "True"),
|
||||
Err("Invalid value for boolean: 'True'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_basic_borders() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "thick,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::Thick,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "slantDashDot,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::SlantDashDot,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "mediumDashDot,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::MediumDashDot,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::MediumDashDotDot,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model
|
||||
.update_range_style(&range, "border.left", "mediumDashed,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::MediumDashed,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_errors() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "border.lef", "thick,#F1F1F1"),
|
||||
Err("Invalid style path: 'border.lef'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "border.left", "thic,#F1F1F1"),
|
||||
Err("Invalid border style: 'thic'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "border.left", "thick,#F1F1F"),
|
||||
Err("Invalid color: '#F1F1F'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "border.left", " "),
|
||||
Err("Invalid border value: ' '.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "border.left", "thick,#F1F1F1,thin"),
|
||||
Err("Invalid border value: 'thick,#F1F1F1,thin'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_removes_border() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
model
|
||||
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(
|
||||
style.border.left,
|
||||
Some(BorderItem {
|
||||
style: BorderStyle::MediumDashDotDot,
|
||||
color: Some("#F1F1F1".to_owned()),
|
||||
})
|
||||
);
|
||||
|
||||
model.update_range_style(&range, "border.left", "").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.border.left, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn false_removes_value() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
let range = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
// bold
|
||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(style.font.b);
|
||||
|
||||
model.update_range_style(&range, "font.b", "false").unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert!(!style.font.b);
|
||||
}
|
||||
30
base/src/test/user_model/test_to_from_bytes.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{test::util::new_empty_model, UserModel};
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut model1 = UserModel::from_model(new_empty_model());
|
||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||
model1.set_column_width(0, 3, width).unwrap();
|
||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||
|
||||
let model_bytes = model1.to_bytes();
|
||||
|
||||
let model2 = UserModel::from_bytes(&model_bytes).unwrap();
|
||||
|
||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
||||
assert_eq!(
|
||||
model2.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("Hello IronCalc!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes();
|
||||
assert_eq!(
|
||||
&UserModel::from_bytes(model_bytes).unwrap_err(),
|
||||
"Error parsing workbook: invalid packing"
|
||||
);
|
||||
}
|
||||
66
base/src/test/user_model/test_undo_redo.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{test::util::new_empty_model, UserModel};
|
||||
|
||||
#[test]
|
||||
fn simple_undo_redo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// at the beginning I cannot undo or redo
|
||||
assert!(!model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
assert!(model.set_user_input(0, 1, 1, "=1+2").is_ok());
|
||||
|
||||
// Once I enter a value I can undo but not redo
|
||||
assert!(model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
|
||||
|
||||
// If I undo, I can't undo anymore, but I can redo
|
||||
assert!(model.undo().is_ok());
|
||||
assert!(!model.can_undo());
|
||||
assert!(model.can_redo());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
||||
|
||||
// If I redo, I have the old value and formula
|
||||
assert!(model.redo().is_ok());
|
||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+2".to_string()));
|
||||
assert!(model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_redo_respect_styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert!(model.set_user_input(0, 1, 1, "100").is_ok());
|
||||
assert!(model.set_user_input(0, 1, 1, "125$").is_ok());
|
||||
// The content of the cell is just the number 125
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
|
||||
assert!(model.undo().is_ok());
|
||||
// The cell has no currency number formatting
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("100".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("100".to_string()));
|
||||
assert!(model.redo().is_ok());
|
||||
// The cell has the number 125 formatted as '125$'
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 1),
|
||||
Ok("125$".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_undo_can_redo() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert!(!model.can_undo());
|
||||
assert!(!model.can_redo());
|
||||
|
||||
assert!(model.undo().is_ok());
|
||||
assert!(model.redo().is_ok());
|
||||
}
|
||||
216
base/src/test/user_model/test_view.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
user_model::SelectedView,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn initial_view() {
|
||||
let model = new_empty_model();
|
||||
let model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_cell_sets_the_range() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 4,
|
||||
range: [5, 4, 5, 4],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_range_does_not_set_the_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_range(5, 4, 10, 6).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [5, 4, 10, 6],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_new_sheet_and_back() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
model.set_selected_sheet(1).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (1, 1, 1));
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(-5, 4),
|
||||
Err("Invalid row: '-5'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(5, -4),
|
||||
Err("Invalid column: '-4'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(-1, 1, 1, 1),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 0, 1, 1),
|
||||
Err("Invalid column: '0'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors_wrong_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully set a wrong index
|
||||
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
|
||||
let mut model = UserModel::from_model(model);
|
||||
// It's returning the wrong number
|
||||
assert_eq!(model.get_selected_sheet(), 2);
|
||||
|
||||
// But we can't set the selected cell anymore
|
||||
assert_eq!(
|
||||
model.set_selected_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(3, 4, 5, 6),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
// we can fix it by setting the right cell
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(3, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_top_left_visible_cell(100, 12).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<SelectedView>(&s).unwrap(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(-100, 12),
|
||||
Err("Invalid row: '-100'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(100, -12),
|
||||
Err("Invalid column: '-12'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_no_views() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully remove the view
|
||||
model.workbook.views = HashMap::new();
|
||||
// also in the sheet
|
||||
model.workbook.worksheets[0].views = HashMap::new();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// get methods will return defaults
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
|
||||
// set methods won't complain. but won't work either
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(5, 6).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
}
|
||||
@@ -32,11 +32,11 @@ impl Model {
|
||||
let cell_reference = self._parse_reference(cell);
|
||||
let column = cell_reference.column;
|
||||
let row = cell_reference.row;
|
||||
self.cell_formula(cell_reference.sheet, row, column)
|
||||
self.get_cell_formula(cell_reference.sheet, row, column)
|
||||
.unwrap()
|
||||
}
|
||||
pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String {
|
||||
self.formatted_cell_value(sheet, row, column).unwrap()
|
||||
self.get_formatted_cell_value(sheet, row, column).unwrap()
|
||||
}
|
||||
pub fn _get_text(&self, cell: &str) -> String {
|
||||
let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell);
|
||||
|
||||
@@ -1,39 +1,18 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
|
||||
// Useful for `#[serde(default = "default_as_true")]`
|
||||
fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_as_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Useful for `#[serde(skip_serializing_if = "is_true")]`
|
||||
fn is_true(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
fn is_zero(num: &i32) -> bool {
|
||||
*num == 0
|
||||
}
|
||||
|
||||
fn is_default_alignment(o: &Option<Alignment>) -> bool {
|
||||
o.is_none() || *o == Some(Alignment::default())
|
||||
}
|
||||
|
||||
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
|
||||
h.values().len() == 0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Metadata {
|
||||
pub application: String,
|
||||
pub app_version: String,
|
||||
@@ -43,14 +22,21 @@ pub struct Metadata {
|
||||
pub last_modified: String, //"2020-11-20T16:24:35"
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct WorkbookSettings {
|
||||
pub tz: String,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
/// A Workbook View tracks of the selected sheet for each view
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct WorkbookView {
|
||||
/// The index of the currently selected sheet.
|
||||
pub sheet: u32,
|
||||
}
|
||||
|
||||
/// An internal representation of an IronCalc Workbook
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Workbook {
|
||||
pub shared_strings: Vec<String>,
|
||||
pub defined_names: Vec<DefinedName>,
|
||||
@@ -59,28 +45,22 @@ pub struct Workbook {
|
||||
pub name: String,
|
||||
pub settings: WorkbookSettings,
|
||||
pub metadata: Metadata,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "hashmap_is_empty")]
|
||||
pub tables: HashMap<String, Table>,
|
||||
pub views: HashMap<u32, WorkbookView>,
|
||||
}
|
||||
|
||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct DefinedName {
|
||||
pub name: String,
|
||||
pub formula: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sheet_id: Option<u32>,
|
||||
}
|
||||
|
||||
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
|
||||
/// Internal representation of a worksheet Excel object
|
||||
|
||||
/// * state:
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SheetState {
|
||||
Visible,
|
||||
Hidden,
|
||||
@@ -97,8 +77,25 @@ impl Display for SheetState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of the worksheet as seen by the user. This includes
|
||||
/// details such as the currently selected cell, the visible range, and the
|
||||
/// position of the viewport.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct WorksheetView {
|
||||
/// The row index of the currently selected cell.
|
||||
pub row: i32,
|
||||
/// The column index of the currently selected cell.
|
||||
pub column: i32,
|
||||
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
|
||||
pub range: [i32; 4],
|
||||
/// The row index of the topmost visible cell in the worksheet view.
|
||||
pub top_row: i32,
|
||||
/// The column index of the leftmost visible cell in the worksheet view.
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
/// Internal representation of a worksheet Excel object
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Worksheet {
|
||||
pub dimension: String,
|
||||
pub cols: Vec<Col>,
|
||||
@@ -108,16 +105,14 @@ pub struct Worksheet {
|
||||
pub shared_formulas: Vec<String>,
|
||||
pub sheet_id: u32,
|
||||
pub state: SheetState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
pub merge_cells: Vec<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub frozen_rows: i32,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub frozen_columns: i32,
|
||||
pub views: HashMap<u32, WorksheetView>,
|
||||
/// Whether or not to show the grid lines in the worksheet
|
||||
pub show_grid_lines: bool,
|
||||
}
|
||||
|
||||
/// Internal representation of Excel's sheet_data
|
||||
@@ -125,7 +120,7 @@ pub struct Worksheet {
|
||||
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.73
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Row {
|
||||
/// Row index
|
||||
pub r: i32,
|
||||
@@ -133,23 +128,19 @@ pub struct Row {
|
||||
pub custom_format: bool,
|
||||
pub custom_height: bool,
|
||||
pub s: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.13
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Col {
|
||||
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
|
||||
/// First column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub min: i32,
|
||||
/// Last column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub max: i32,
|
||||
|
||||
pub width: f64,
|
||||
pub custom_width: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -164,32 +155,55 @@ pub enum CellType {
|
||||
CompoundData = 128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
#[serde(tag = "t", deny_unknown_fields)]
|
||||
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
||||
pub enum Cell {
|
||||
#[serde(rename = "empty")]
|
||||
EmptyCell { s: i32 },
|
||||
#[serde(rename = "b")]
|
||||
BooleanCell { v: bool, s: i32 },
|
||||
#[serde(rename = "n")]
|
||||
NumberCell { v: f64, s: i32 },
|
||||
EmptyCell {
|
||||
s: i32,
|
||||
},
|
||||
|
||||
BooleanCell {
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
NumberCell {
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
// Maybe we should not have this type. In Excel this is just a string
|
||||
#[serde(rename = "e")]
|
||||
ErrorCell { ei: Error, s: i32 },
|
||||
ErrorCell {
|
||||
ei: Error,
|
||||
s: i32,
|
||||
},
|
||||
// Always a shared string
|
||||
#[serde(rename = "s")]
|
||||
SharedString { si: i32, s: i32 },
|
||||
SharedString {
|
||||
si: i32,
|
||||
s: i32,
|
||||
},
|
||||
// Non evaluated Formula
|
||||
#[serde(rename = "u")]
|
||||
CellFormula { f: i32, s: i32 },
|
||||
#[serde(rename = "fb")]
|
||||
CellFormulaBoolean { f: i32, v: bool, s: i32 },
|
||||
#[serde(rename = "fn")]
|
||||
CellFormulaNumber { f: i32, v: f64, s: i32 },
|
||||
CellFormula {
|
||||
f: i32,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaBoolean {
|
||||
f: i32,
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaNumber {
|
||||
f: i32,
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
// always inline string
|
||||
#[serde(rename = "str")]
|
||||
CellFormulaString { f: i32, v: String, s: i32 },
|
||||
#[serde(rename = "fe")]
|
||||
CellFormulaString {
|
||||
f: i32,
|
||||
v: String,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaError {
|
||||
f: i32,
|
||||
ei: Error,
|
||||
@@ -208,17 +222,16 @@ impl Default for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub text: String,
|
||||
pub author_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author_id: Option<String>,
|
||||
pub cell_ref: String,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.5.1.2
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Table {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
@@ -226,34 +239,24 @@ pub struct Table {
|
||||
pub reference: String,
|
||||
pub totals_row_count: u32,
|
||||
pub header_row_count: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
pub columns: Vec<TableColumn>,
|
||||
pub style_info: TableStyleInfo,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub has_filters: bool,
|
||||
}
|
||||
|
||||
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
|
||||
// the totals_row_function is an enum not String methinks
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TableColumn {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_function: Option<String>,
|
||||
}
|
||||
|
||||
@@ -271,25 +274,16 @@ impl Default for TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct TableStyleInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_first_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_last_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_row_stripes: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_column_stripes: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Styles {
|
||||
pub num_fmts: Vec<NumFmt>,
|
||||
pub fonts: Vec<Font>,
|
||||
@@ -314,8 +308,9 @@ impl Default for Styles {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Style {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub alignment: Option<Alignment>,
|
||||
pub num_fmt: String,
|
||||
pub fill: Fill,
|
||||
@@ -324,7 +319,7 @@ pub struct Style {
|
||||
pub quote_prefix: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NumFmt {
|
||||
pub num_fmt_id: i32,
|
||||
pub format_code: String,
|
||||
@@ -342,7 +337,7 @@ impl Default for NumFmt {
|
||||
// ST_FontScheme simple type (§18.18.33).
|
||||
// Usually major fonts are used for styles like headings,
|
||||
// and minor fonts are used for body and paragraph text.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Default)]
|
||||
pub enum FontScheme {
|
||||
@@ -362,7 +357,7 @@ impl Display for FontScheme {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Font {
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
@@ -405,7 +400,7 @@ impl Default for Font {
|
||||
}
|
||||
|
||||
// TODO: Maybe use an enum for the pattern_type values here?
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Fill {
|
||||
pub pattern_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -424,7 +419,7 @@ impl Default for Fill {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum HorizontalAlignment {
|
||||
Center,
|
||||
@@ -466,7 +461,7 @@ impl Display for HorizontalAlignment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum VerticalAlignment {
|
||||
Bottom,
|
||||
@@ -501,7 +496,7 @@ impl Display for VerticalAlignment {
|
||||
}
|
||||
|
||||
// 1762
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Alignment {
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
|
||||
@@ -514,29 +509,17 @@ pub struct Alignment {
|
||||
pub wrap_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyleXfs {
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_fill: bool,
|
||||
}
|
||||
|
||||
@@ -557,39 +540,24 @@ impl Default for CellStyleXfs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct CellXfs {
|
||||
pub xf_id: i32,
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_fill: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub quote_prefix: bool,
|
||||
#[serde(skip_serializing_if = "is_default_alignment")]
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyles {
|
||||
pub name: String,
|
||||
pub xf_id: i32,
|
||||
@@ -606,7 +574,7 @@ impl Default for CellStyles {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BorderStyle {
|
||||
Thin,
|
||||
@@ -636,13 +604,13 @@ impl Display for BorderStyle {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct BorderItem {
|
||||
pub style: BorderStyle,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Border {
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
@@ -665,9 +633,10 @@ pub struct Border {
|
||||
/// Information need to show a sheet tab in the UI
|
||||
/// The color is serialized only if it is not Color::None
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct SheetInfo {
|
||||
pub struct SheetProperties {
|
||||
pub name: String,
|
||||
pub state: String,
|
||||
pub sheet_id: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
1690
base/src/user_model.rs
Normal file
@@ -27,16 +27,4 @@ impl Workbook {
|
||||
.get_mut(worksheet_index as usize)
|
||||
.ok_or_else(|| "Invalid sheet index".to_string())
|
||||
}
|
||||
|
||||
pub fn get_worksheets_info(&self) -> Vec<SheetInfo> {
|
||||
self.worksheets
|
||||
.iter()
|
||||
.map(|worksheet| SheetInfo {
|
||||
name: worksheet.get_name(),
|
||||
state: worksheet.state.to_string(),
|
||||
color: worksheet.color.clone(),
|
||||
sheet_id: worksheet.sheet_id,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Worksheet {
|
||||
self.sheet_data.get_mut(&row)?.get_mut(&column)
|
||||
}
|
||||
|
||||
fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
|
||||
pub(crate) fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
|
||||
match self.sheet_data.get_mut(&row) {
|
||||
Some(column_data) => match column_data.get(&column) {
|
||||
Some(_cell) => {
|
||||
@@ -68,9 +68,8 @@ impl Worksheet {
|
||||
if row.r == row_index {
|
||||
if row.custom_format {
|
||||
return row.s;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cols = &self.cols;
|
||||
@@ -106,64 +105,8 @@ impl Worksheet {
|
||||
}
|
||||
|
||||
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
|
||||
let cols = &mut self.cols;
|
||||
let col = Col {
|
||||
min: column,
|
||||
max: column,
|
||||
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
|
||||
custom_width: true,
|
||||
style: Some(style_index),
|
||||
};
|
||||
let mut index = 0;
|
||||
let mut split = false;
|
||||
for c in cols.iter_mut() {
|
||||
let min = c.min;
|
||||
let max = c.max;
|
||||
if min <= column && column <= max {
|
||||
if min == column && max == column {
|
||||
c.style = Some(style_index);
|
||||
return Ok(());
|
||||
} else {
|
||||
// We need to split the result
|
||||
split = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if column < min {
|
||||
// We passed, we should insert at index
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if split {
|
||||
let min = cols[index].min;
|
||||
let max = cols[index].max;
|
||||
let pre = Col {
|
||||
min,
|
||||
max: column - 1,
|
||||
width: cols[index].width,
|
||||
custom_width: cols[index].custom_width,
|
||||
style: cols[index].style,
|
||||
};
|
||||
let post = Col {
|
||||
min: column + 1,
|
||||
max,
|
||||
width: cols[index].width,
|
||||
custom_width: cols[index].custom_width,
|
||||
style: cols[index].style,
|
||||
};
|
||||
cols.remove(index);
|
||||
if column != max {
|
||||
cols.insert(index, post);
|
||||
}
|
||||
cols.insert(index, col);
|
||||
if column != min {
|
||||
cols.insert(index, pre);
|
||||
}
|
||||
} else {
|
||||
cols.insert(index, col);
|
||||
}
|
||||
Ok(())
|
||||
let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
|
||||
self.set_column_width_and_style(column, width, Some(style_index))
|
||||
}
|
||||
|
||||
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
|
||||
@@ -191,7 +134,7 @@ impl Worksheet {
|
||||
cell.set_style(style_index);
|
||||
}
|
||||
None => {
|
||||
self.set_cell_empty_with_style(row, column, style_index);
|
||||
self.cell_clear_contents_with_style(row, column, style_index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,13 +166,13 @@ impl Worksheet {
|
||||
self.update_cell(row, column, cell);
|
||||
}
|
||||
|
||||
pub fn set_cell_empty(&mut self, row: i32, column: i32) {
|
||||
pub fn cell_clear_contents(&mut self, row: i32, column: i32) {
|
||||
let s = self.get_style(row, column);
|
||||
let cell = Cell::EmptyCell { s };
|
||||
self.update_cell(row, column, cell);
|
||||
}
|
||||
|
||||
pub fn set_cell_empty_with_style(&mut self, row: i32, column: i32, style: i32) {
|
||||
pub fn cell_clear_contents_with_style(&mut self, row: i32, column: i32, style: i32) {
|
||||
let cell = Cell::EmptyCell { s: style };
|
||||
self.update_cell(row, column, cell);
|
||||
}
|
||||
@@ -237,7 +180,8 @@ impl Worksheet {
|
||||
pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> {
|
||||
if frozen_rows < 0 {
|
||||
return Err("Frozen rows cannot be negative".to_string());
|
||||
} else if frozen_rows >= constants::LAST_ROW {
|
||||
}
|
||||
if frozen_rows >= constants::LAST_ROW {
|
||||
return Err("Too many rows".to_string());
|
||||
}
|
||||
self.frozen_rows = frozen_rows;
|
||||
@@ -247,7 +191,8 @@ impl Worksheet {
|
||||
pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> {
|
||||
if frozen_columns < 0 {
|
||||
return Err("Frozen columns cannot be negative".to_string());
|
||||
} else if frozen_columns >= constants::LAST_COLUMN {
|
||||
}
|
||||
if frozen_columns >= constants::LAST_COLUMN {
|
||||
return Err("Too many columns".to_string());
|
||||
}
|
||||
self.frozen_columns = frozen_columns;
|
||||
@@ -281,11 +226,21 @@ impl Worksheet {
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Changes the width of a column.
|
||||
/// * If the column does not a have a width we simply add it
|
||||
/// * If it has, it might be part of a range and we ned to split the range.
|
||||
/// Fails if column index is outside allowed range.
|
||||
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
|
||||
self.set_column_width_and_style(column, width, None)
|
||||
}
|
||||
|
||||
pub(crate) fn set_column_width_and_style(
|
||||
&mut self,
|
||||
column: i32,
|
||||
width: f64,
|
||||
style: Option<i32>,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Column number '{column}' is not valid."));
|
||||
}
|
||||
@@ -295,7 +250,7 @@ impl Worksheet {
|
||||
max: column,
|
||||
width: width / constants::COLUMN_WIDTH_FACTOR,
|
||||
custom_width: true,
|
||||
style: None,
|
||||
style,
|
||||
};
|
||||
let mut index = 0;
|
||||
let mut split = false;
|
||||
@@ -306,11 +261,9 @@ impl Worksheet {
|
||||
if min == column && max == column {
|
||||
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
||||
return Ok(());
|
||||
} else {
|
||||
// We need to split the result
|
||||
split = true;
|
||||
break;
|
||||
}
|
||||
split = true;
|
||||
break;
|
||||
}
|
||||
if column < min {
|
||||
// We passed, we should insert at index
|
||||
@@ -351,7 +304,7 @@ impl Worksheet {
|
||||
}
|
||||
|
||||
/// Return the width of a column in pixels
|
||||
pub fn column_width(&self, column: i32) -> Result<f64, String> {
|
||||
pub fn get_column_width(&self, column: i32) -> Result<f64, String> {
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Column number '{column}' is not valid."));
|
||||
}
|
||||
@@ -363,9 +316,8 @@ impl Worksheet {
|
||||
if column >= min && column <= max {
|
||||
if col.custom_width {
|
||||
return Ok(col.width * constants::COLUMN_WIDTH_FACTOR);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
||||
|
||||
1
bindings/wasm/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/*
|
||||
23
bindings/wasm/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "wasm"
|
||||
version = "0.1.3"
|
||||
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
||||
description = "IronCalc Web bindings"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/ironcalc/web-bindings"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Uses `../ironcalc/base` when used locally, and uses
|
||||
# the inicated version from crates.io when published.
|
||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
||||
ironcalc_base = { path = "../../base", version = "0.1" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
wasm-bindgen = "0.2.92"
|
||||
serde-wasm-bindgen = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.38"
|
||||
20
bindings/wasm/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
all:
|
||||
wasm-pack build --target web --scope ironcalc --release
|
||||
cp README.pkg.md pkg/README.md
|
||||
tsc types.ts --target esnext --module esnext
|
||||
python fix_types.py
|
||||
|
||||
tests:
|
||||
wasm-pack build --target nodejs && node tests/test.mjs
|
||||
|
||||
lint:
|
||||
cargo check
|
||||
cargo fmt -- --check
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
rm -rf pkg
|
||||
rm -f types.js
|
||||
|
||||
.PHONY: all lint clean
|
||||
31
bindings/wasm/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# IronCalc Web bindings
|
||||
|
||||
This crate is used to build the web bindings for IronCalc.
|
||||
Note that it does not contain the xlsx writer and reader, only the engine.
|
||||
|
||||
https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Right now this is a manual process and only carries out a smoke test:
|
||||
|
||||
1. Build the package
|
||||
2. Run `python -m http.server`
|
||||
3. In your browser open <http://0.0.0.0:8000/test.html>
|
||||
|
||||
## Publishing
|
||||
|
||||
Follow the commands:
|
||||
|
||||
```bash
|
||||
wasm-pack login
|
||||
make
|
||||
cd pkg
|
||||
npm publish --access=public
|
||||
```
|
||||
33
bindings/wasm/README.pkg.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# IronCalc Web bindings
|
||||
|
||||
This package contains web bindings for IronCalc. Note that it does not contain the xlsx writer and reader, only the engine.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
In your project
|
||||
|
||||
```
|
||||
npm install @ironcalc/wasm
|
||||
```
|
||||
|
||||
And then in your TypeScript
|
||||
|
||||
```TypeScript
|
||||
import init, { Model } from "@ironcalc/wasm";
|
||||
|
||||
await init();
|
||||
|
||||
function compute() {
|
||||
const model = new Model('en', 'UTC');
|
||||
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
||||
|
||||
const result = model.getFormattedCellValue(0, 1, 2);
|
||||
|
||||
console.log("Result: ", result);
|
||||
}
|
||||
|
||||
compute();
|
||||
```
|
||||
147
bindings/wasm/fix_types.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# Regrettably at the time of writing there is not a perfect way to
|
||||
# generate the TypeScript types from Rust so we basically fix them manually
|
||||
# Hopefully this will suffice for our needs and one day will be automatic
|
||||
|
||||
header = r"""
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
""".strip()
|
||||
|
||||
get_tokens_str = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getTokens(formula: string): any;
|
||||
""".strip()
|
||||
|
||||
get_tokens_str_types = r"""
|
||||
* @returns {MarkedToken[]}
|
||||
*/
|
||||
export function getTokens(formula: string): MarkedToken[];
|
||||
""".strip()
|
||||
|
||||
update_style_str = r"""
|
||||
/**
|
||||
* @param {any} range
|
||||
* @param {string} style_path
|
||||
* @param {string} value
|
||||
*/
|
||||
updateRangeStyle(range: any, style_path: string, value: string): void;
|
||||
""".strip()
|
||||
|
||||
update_style_str_types = r"""
|
||||
/**
|
||||
* @param {Area} range
|
||||
* @param {string} style_path
|
||||
* @param {string} value
|
||||
*/
|
||||
updateRangeStyle(range: Area, style_path: string, value: string): void;
|
||||
""".strip()
|
||||
|
||||
properties = r"""
|
||||
/**
|
||||
* @returns {any}
|
||||
*/
|
||||
getWorksheetsProperties(): any;
|
||||
""".strip()
|
||||
|
||||
properties_types = r"""
|
||||
/**
|
||||
* @returns {WorksheetProperties[]}
|
||||
*/
|
||||
getWorksheetsProperties(): WorksheetProperties[];
|
||||
""".strip()
|
||||
|
||||
style = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getCellStyle(sheet: number, row: number, column: number): any;
|
||||
""".strip()
|
||||
|
||||
style_types = r"""
|
||||
* @returns {CellStyle}
|
||||
*/
|
||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||
""".strip()
|
||||
|
||||
view = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getSelectedView(): any;
|
||||
""".strip()
|
||||
|
||||
view_types = r"""
|
||||
* @returns {CellStyle}
|
||||
*/
|
||||
getSelectedView(): SelectedView;
|
||||
""".strip()
|
||||
|
||||
autofill_rows = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: any, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_rows_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: Area, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: any, to_column: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: Area, to_column: number): void;
|
||||
"""
|
||||
|
||||
def fix_types(text):
|
||||
text = text.replace(get_tokens_str, get_tokens_str_types)
|
||||
text = text.replace(update_style_str, update_style_str_types)
|
||||
text = text.replace(properties, properties_types)
|
||||
text = text.replace(style, style_types)
|
||||
text = text.replace(view, view_types)
|
||||
text = text.replace(autofill_rows, autofill_rows_types)
|
||||
text = text.replace(autofill_columns, autofill_columns_types)
|
||||
with open("types.ts") as f:
|
||||
types_str = f.read()
|
||||
header_types = "{}\n\n{}".format(header, types_str)
|
||||
text = text.replace(header, header_types)
|
||||
if text.find("any") != -1:
|
||||
print("There are 'unfixed' types. Please check.")
|
||||
exit(1)
|
||||
return text
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
types_file = "pkg/wasm.d.ts"
|
||||
with open(types_file) as f:
|
||||
text = f.read()
|
||||
text = fix_types(text)
|
||||
with open(types_file, "wb") as f:
|
||||
f.write(bytes(text, "utf8"))
|
||||
|
||||
js_file = "pkg/wasm.js"
|
||||
with open("types.js") as f:
|
||||
text_js = f.read()
|
||||
with open(js_file) as f:
|
||||
text = f.read()
|
||||
|
||||
with open(js_file, "wb") as f:
|
||||
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
||||
|
||||
|
||||
|
||||
379
bindings/wasm/src/lib.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use wasm_bindgen::{
|
||||
prelude::{wasm_bindgen, JsError},
|
||||
JsValue,
|
||||
};
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||
types::CellType,
|
||||
UserModel as BaseModel,
|
||||
};
|
||||
|
||||
fn to_js_error(error: String) -> JsError {
|
||||
JsError::new(&error.to_string())
|
||||
}
|
||||
|
||||
/// Return an array with a list of all the tokens from a formula
|
||||
/// This is used by the UI to color them according to a theme.
|
||||
#[wasm_bindgen(js_name = "getTokens")]
|
||||
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
||||
let tokens = tokenizer(formula);
|
||||
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub struct Model {
|
||||
model: BaseModel,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Model {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(locale: &str, timezone: &str) -> Result<Model, JsError> {
|
||||
let model = BaseModel::new_empty("workbook", locale, timezone).map_err(to_js_error)?;
|
||||
Ok(Model { model })
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Model, JsError> {
|
||||
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
|
||||
Ok(Model { model })
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Result<(), JsError> {
|
||||
self.model.undo().map_err(to_js_error)
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Result<(), JsError> {
|
||||
self.model.redo().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "canUndo")]
|
||||
pub fn can_undo(&self) -> bool {
|
||||
self.model.can_undo()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "canRedo")]
|
||||
pub fn can_redo(&self) -> bool {
|
||||
self.model.can_redo()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "pauseEvaluation")]
|
||||
pub fn pause_evaluation(&mut self) {
|
||||
self.model.pause_evaluation()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "resumeEvaluation")]
|
||||
pub fn resume_evaluation(&mut self) {
|
||||
self.model.resume_evaluation()
|
||||
}
|
||||
|
||||
pub fn evaluate(&mut self) {
|
||||
self.model.evaluate();
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "flushSendQueue")]
|
||||
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
||||
self.model.flush_send_queue()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "applyExternalDiffs")]
|
||||
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<(), JsError> {
|
||||
self.model.apply_external_diffs(diffs).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getCellContent")]
|
||||
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, JsError> {
|
||||
self.model
|
||||
.get_cell_content(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "newSheet")]
|
||||
pub fn new_sheet(&mut self) {
|
||||
self.model.new_sheet()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "deleteSheet")]
|
||||
pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
|
||||
self.model.delete_sheet(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "renameSheet")]
|
||||
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
|
||||
self.model.rename_sheet(sheet, name).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "rangeClearAll")]
|
||||
pub fn range_clear_all(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let range = Area {
|
||||
sheet,
|
||||
row: start_row,
|
||||
column: start_column,
|
||||
width: end_column - start_column + 1,
|
||||
height: end_row - start_row + 1,
|
||||
};
|
||||
self.model.range_clear_all(&range).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "rangeClearContents")]
|
||||
pub fn range_clear_contents(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let range = Area {
|
||||
sheet,
|
||||
row: start_row,
|
||||
column: start_column,
|
||||
width: end_column - start_column + 1,
|
||||
height: end_row - start_row + 1,
|
||||
};
|
||||
self.model.range_clear_contents(&range).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "insertRow")]
|
||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "insertColumn")]
|
||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "deleteRow")]
|
||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "deleteColumn")]
|
||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setRowHeight")]
|
||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_row_height(sheet, row, height)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setColumnWidth")]
|
||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_column_width(sheet, column, width)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getRowHeight")]
|
||||
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64, JsError> {
|
||||
self.model.get_row_height(sheet, row).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getColumnWidth")]
|
||||
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64, JsError> {
|
||||
self.model
|
||||
.get_column_width(sheet, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setUserInput")]
|
||||
pub fn set_user_input(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
input: &str,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_user_input(sheet, row, column, input)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getFormattedCellValue")]
|
||||
pub fn get_formatted_cell_value(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<String, JsError> {
|
||||
self.model
|
||||
.get_formatted_cell_value(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getFrozenRowsCount")]
|
||||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, JsError> {
|
||||
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getFrozenColumnsCount")]
|
||||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, JsError> {
|
||||
self.model
|
||||
.get_frozen_columns_count(sheet)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setFrozenRowsCount")]
|
||||
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_frozen_rows_count(sheet, count)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setFrozenColumnsCount")]
|
||||
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_frozen_columns_count(sheet, count)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||
pub fn update_range_style(
|
||||
&mut self,
|
||||
range: JsValue,
|
||||
style_path: &str,
|
||||
value: &str,
|
||||
) -> Result<(), JsError> {
|
||||
let range: Area =
|
||||
serde_wasm_bindgen::from_value(range).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.update_range_style(&range, style_path, value)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getCellStyle")]
|
||||
pub fn get_cell_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
) -> Result<JsValue, JsError> {
|
||||
self.model
|
||||
.get_cell_style(sheet, row, column)
|
||||
.map_err(to_js_error)
|
||||
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getCellType")]
|
||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
|
||||
Ok(
|
||||
match self
|
||||
.model
|
||||
.get_cell_type(sheet, row, column)
|
||||
.map_err(to_js_error)?
|
||||
{
|
||||
CellType::Number => 1,
|
||||
CellType::Text => 2,
|
||||
CellType::LogicalValue => 4,
|
||||
CellType::ErrorValue => 16,
|
||||
CellType::Array => 64,
|
||||
CellType::CompoundData => 128,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
|
||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedSheet")]
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
self.model.get_selected_sheet()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedCell")]
|
||||
pub fn get_selected_cell(&self) -> Vec<i32> {
|
||||
let (sheet, row, column) = self.model.get_selected_cell();
|
||||
vec![sheet as i32, row, column]
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedView")]
|
||||
pub fn get_selected_view(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedSheet")]
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
|
||||
self.model.set_selected_sheet(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedCell")]
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_cell(row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedRange")]
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_range(start_row, start_column, end_row, end_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setTopLeftVisibleCell")]
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
top_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_top_left_visible_cell(top_row, top_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setShowGridLines")]
|
||||
pub fn set_show_grid_lines(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
show_grid_lines: bool,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_show_grid_lines(sheet, show_grid_lines)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getShowGridLines")]
|
||||
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool, JsError> {
|
||||
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_rows(&area, to_row)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||
pub fn auto_fill_columns(
|
||||
&mut self,
|
||||
source_area: JsValue,
|
||||
to_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_columns(&area, to_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
}
|
||||
28
bindings/wasm/test.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test IronCalc Web Bindings</title>
|
||||
<script type="module">
|
||||
import init, { Model } from "./pkg/wasm.js";
|
||||
|
||||
await init();
|
||||
|
||||
function test() {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
||||
|
||||
const result = model.getFormattedCellValue(0, 1, 2);
|
||||
console.assert(result === "70");
|
||||
console.log("Hoooray! Tests passed");
|
||||
}
|
||||
|
||||
test();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div>Please have a look at the console</div>
|
||||
</body>
|
||||
</html>
|
||||
132
bindings/wasm/tests/test.mjs
Normal file
@@ -0,0 +1,132 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert'
|
||||
import { Model } from "../pkg/wasm.js";
|
||||
|
||||
test('Frozen rows and columns', () => {
|
||||
let model = new Model('en', 'UTC');
|
||||
assert.strictEqual(model.getFrozenRowsCount(0), 0);
|
||||
assert.strictEqual(model.getFrozenColumnsCount(0), 0);
|
||||
|
||||
model.setFrozenColumnsCount(0, 4);
|
||||
model.setFrozenRowsCount(0, 3)
|
||||
|
||||
assert.strictEqual(model.getFrozenRowsCount(0), 3);
|
||||
assert.strictEqual(model.getFrozenColumnsCount(0), 4);
|
||||
});
|
||||
|
||||
test('Row height', () => {
|
||||
let model = new Model('en', 'UTC');
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 21);
|
||||
|
||||
model.setRowHeight(0, 3, 32);
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||
|
||||
model.undo();
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 21);
|
||||
|
||||
model.redo();
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||
|
||||
model.setRowHeight(0, 3, 320);
|
||||
assert.strictEqual(model.getRowHeight(0, 3), 320);
|
||||
});
|
||||
|
||||
test('Evaluates correctly', (t) => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
||||
|
||||
const result = model.getFormattedCellValue(0, 1, 2);
|
||||
assert.strictEqual(result, "70");
|
||||
});
|
||||
|
||||
test('Styles work', () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
let style = model.getCellStyle(0, 1, 1);
|
||||
assert.deepEqual(style, {
|
||||
num_fmt: 'general',
|
||||
fill: { pattern_type: 'none' },
|
||||
font: {
|
||||
sz: 11,
|
||||
color: '#000000',
|
||||
name: 'Calibri',
|
||||
family: 2,
|
||||
scheme: 'minor'
|
||||
},
|
||||
border: {},
|
||||
quote_prefix: false
|
||||
});
|
||||
model.setUserInput(0, 1, 1, "'=1+1");
|
||||
style = model.getCellStyle(0, 1, 1);
|
||||
assert.deepEqual(style, {
|
||||
num_fmt: 'general',
|
||||
fill: { pattern_type: 'none' },
|
||||
font: {
|
||||
sz: 11,
|
||||
color: '#000000',
|
||||
name: 'Calibri',
|
||||
family: 2,
|
||||
scheme: 'minor'
|
||||
},
|
||||
border: {},
|
||||
quote_prefix: true
|
||||
});
|
||||
});
|
||||
|
||||
test("Add sheets", (t) => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.newSheet();
|
||||
model.renameSheet(1, "NewName");
|
||||
let props = model.getWorksheetsProperties();
|
||||
assert.deepEqual(props, [{
|
||||
name: 'Sheet1',
|
||||
sheet_id: 1,
|
||||
state: 'visible'
|
||||
},
|
||||
{
|
||||
name: 'NewName',
|
||||
sheet_id: 2,
|
||||
state: 'visible'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("invalid sheet index throws an exception", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
assert.throws(() => {
|
||||
model.setRowHeight(1, 1, 100);
|
||||
}, {
|
||||
name: 'Error',
|
||||
message: 'Invalid sheet index',
|
||||
});
|
||||
});
|
||||
|
||||
test("invalid column throws an exception", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
assert.throws(() => {
|
||||
model.setRowHeight(0, -1, 100);
|
||||
}, {
|
||||
name: 'Error',
|
||||
message: "Row number '-1' is not valid.",
|
||||
});
|
||||
});
|
||||
|
||||
test("floating column numbers get truncated", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setRowHeight(0.8, 5.2, 100.5);
|
||||
|
||||
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
|
||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||
});
|
||||
|
||||
test("autofill", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.autoFillRows({sheet: 0, row: 1, column: 1, width: 1, height: 1}, 2);
|
||||
|
||||
const result = model.getFormattedCellValue(0, 2, 1);
|
||||
assert.strictEqual(result, "23");
|
||||
});
|
||||
|
||||
|
||||
|
||||
194
bindings/wasm/types.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
export interface Area {
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type ErrorType =
|
||||
| "REF"
|
||||
| "NAME"
|
||||
| "VALUE"
|
||||
| "DIV"
|
||||
| "NA"
|
||||
| "NUM"
|
||||
| "ERROR"
|
||||
| "NIMPL"
|
||||
| "SPILL"
|
||||
| "CALC"
|
||||
| "CIRC";
|
||||
|
||||
type OpCompareType =
|
||||
| "LessThan"
|
||||
| "GreaterThan"
|
||||
| "Equal"
|
||||
| "LessOrEqualThan"
|
||||
| "GreaterOrEqualThan"
|
||||
| "NonEqual";
|
||||
|
||||
type OpSumType = "Add" | "Minus";
|
||||
|
||||
type OpProductType = "Times" | "Divide";
|
||||
|
||||
interface ReferenceType {
|
||||
sheet: string | null;
|
||||
row: number;
|
||||
column: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface ParsedReferenceType {
|
||||
column: number;
|
||||
row: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface Reference {
|
||||
Reference: ReferenceType;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
Range: {
|
||||
sheet: string | null;
|
||||
left: ParsedReferenceType;
|
||||
right: ParsedReferenceType;
|
||||
};
|
||||
}
|
||||
|
||||
export type TokenType =
|
||||
| "Illegal"
|
||||
| "Eof"
|
||||
| { Ident: string }
|
||||
| { String: string }
|
||||
| { Boolean: boolean }
|
||||
| { Number: number }
|
||||
| { ERROR: ErrorType }
|
||||
| { COMPARE: OpCompareType }
|
||||
| { SUM: OpSumType }
|
||||
| { PRODUCT: OpProductType }
|
||||
| "POWER"
|
||||
| "LPAREN"
|
||||
| "RPAREN"
|
||||
| "COLON"
|
||||
| "SEMICOLON"
|
||||
| "LBRACKET"
|
||||
| "RBRACKET"
|
||||
| "LBRACE"
|
||||
| "RBRACE"
|
||||
| "COMMA"
|
||||
| "BANG"
|
||||
| "PERCENT"
|
||||
| "AND"
|
||||
| Reference
|
||||
| Range;
|
||||
|
||||
export interface MarkedToken {
|
||||
token: TokenType;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface WorksheetProperties {
|
||||
name: string;
|
||||
color: string;
|
||||
sheet_id: number;
|
||||
}
|
||||
|
||||
interface CellStyleFill {
|
||||
pattern_type: string;
|
||||
fg_color?: string;
|
||||
bg_color?: string;
|
||||
}
|
||||
|
||||
interface CellStyleFont {
|
||||
u: boolean;
|
||||
b: boolean;
|
||||
i: boolean;
|
||||
strike: boolean;
|
||||
sz: number;
|
||||
color: string;
|
||||
name: string;
|
||||
family: number;
|
||||
scheme: string;
|
||||
}
|
||||
|
||||
export enum BorderType {
|
||||
BorderAll,
|
||||
BorderInner,
|
||||
BorderCenterH,
|
||||
BorderCenterV,
|
||||
BorderOuter,
|
||||
BorderNone,
|
||||
BorderTop,
|
||||
BorderRight,
|
||||
BorderBottom,
|
||||
BorderLeft,
|
||||
None,
|
||||
}
|
||||
|
||||
export interface BorderOptions {
|
||||
color: string;
|
||||
style: BorderStyle;
|
||||
border: BorderType;
|
||||
}
|
||||
|
||||
export enum BorderStyle {
|
||||
Thin = "thin",
|
||||
Medium = "medium",
|
||||
Thick = "thick",
|
||||
Dashed = "dashed",
|
||||
Dotted = "dotted",
|
||||
Double = "double",
|
||||
None = "none",
|
||||
}
|
||||
|
||||
interface BorderItem {
|
||||
style: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface CellStyleBorder {
|
||||
diagonal_up?: boolean;
|
||||
diagonal_down?: boolean;
|
||||
left: BorderItem;
|
||||
right: BorderItem;
|
||||
top: BorderItem;
|
||||
bottom: BorderItem;
|
||||
diagonal: BorderItem;
|
||||
}
|
||||
|
||||
export type VerticalAlignment =
|
||||
| "bottom"
|
||||
| "center"
|
||||
| "distributed"
|
||||
| "justify"
|
||||
| "top";
|
||||
|
||||
export type HorizontalAlignment =
|
||||
| "left"
|
||||
| "center"
|
||||
| "right"
|
||||
| "general"
|
||||
| "centerContinuous"
|
||||
| "distributed"
|
||||
| "fill"
|
||||
| "justify";
|
||||
|
||||
interface Alignment {
|
||||
horizontal: HorizontalAlignment;
|
||||
vertical: VerticalAlignment;
|
||||
wrap_text: boolean;
|
||||
}
|
||||
|
||||
export interface CellStyle {
|
||||
read_only: boolean;
|
||||
quote_prefix: boolean;
|
||||
fill: CellStyleFill;
|
||||
font: CellStyleFont;
|
||||
border: CellStyleBorder;
|
||||
num_fmt: string;
|
||||
alignment?: Alignment;
|
||||
}
|
||||
3
codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
ignore:
|
||||
- "xlsx/src/bin"
|
||||
- "bindings/wasm"
|
||||
12
tironcalc/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "tiron"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.27.0"
|
||||
ironcalc = { path = "../xlsx"}
|
||||
ratatui = "0.26.2"
|
||||
tui-input = "0.8.0"
|
||||
52
tironcalc/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# TironCalc
|
||||
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/1206947691058171904.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/zZYWfh3RHJ
|
||||
|
||||
TironCalc, or Tiron for friends, is a TUI (Terminal User Interface) for IronCalc. Based on [ratatui](https://github.com/ratatui-org/ratatui)
|
||||
|
||||

|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
You will find the binary at `./target/release/tiron`.
|
||||
|
||||
## Documentation
|
||||
|
||||
Start empty project:
|
||||
|
||||
```
|
||||
$ tiron
|
||||
```
|
||||
|
||||
Load an existing Excel file:
|
||||
|
||||
```
|
||||
$ tiron example.xlsx
|
||||
```
|
||||
|
||||
- `e` to edit a cell and enter the value or formula.
|
||||
- `q` to quit and save
|
||||
- `+` to add a sheet
|
||||
- `s` to go to the next sheet
|
||||
- `PgUp/PgDown` to navigate rows faster
|
||||
- `u` undo changes
|
||||
- `U` redo changes
|
||||
- `r` insert row
|
||||
- `c` insert column
|
||||
- `C` delete column
|
||||
- `R` delete row
|
||||
- `
|
||||
|
||||
|
||||
## Inspiration
|
||||
|
||||
James Gosling of Java fame created [sc](https://en.wikipedia.org/wiki/Sc_(spreadsheet_calculator)) the spreadsheet calculator.
|
||||
|
||||
Andrés Martinelli has been maintaining [sc-im](https://github.com/andmarti1424/sc-im), the spreadsheet calculator improvised.
|
||||
BIN
tironcalc/screenshot.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
556
tironcalc/src/main.rs
Normal file
@@ -0,0 +1,556 @@
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event as CEvent, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ironcalc::{
|
||||
base::{expressions::utils::number_to_column, Model, UserModel},
|
||||
export::save_to_xlsx,
|
||||
import::{load_from_icalc, load_from_xlsx},
|
||||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Cell, Clear, Paragraph, Row, Table},
|
||||
Terminal,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{io, sync::mpsc};
|
||||
use tui_input::{backend::crossterm::EventHandler, Input};
|
||||
|
||||
use std::env;
|
||||
|
||||
enum Event<I> {
|
||||
Input(I),
|
||||
Tick,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CursorMode {
|
||||
Navigate,
|
||||
Input,
|
||||
Popup,
|
||||
}
|
||||
|
||||
struct SelectedRange {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
min_row: i32,
|
||||
min_column: i32,
|
||||
max_row: i32,
|
||||
max_column: i32,
|
||||
}
|
||||
|
||||
struct SheetState {
|
||||
row: i32,
|
||||
column: i32,
|
||||
min_row: i32,
|
||||
min_column: i32,
|
||||
max_row: i32,
|
||||
max_column: i32,
|
||||
}
|
||||
|
||||
struct ModelState {
|
||||
selected_sheet: u32,
|
||||
sheet_states: Vec<SheetState>,
|
||||
}
|
||||
|
||||
impl ModelState {
|
||||
pub fn new(sheet_count: usize) -> ModelState {
|
||||
let mut sheet_states = vec![];
|
||||
for _ in 0..sheet_count {
|
||||
sheet_states.push(SheetState {
|
||||
row: 1,
|
||||
column: 1,
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
max_row: 1,
|
||||
max_column: 1,
|
||||
});
|
||||
}
|
||||
ModelState {
|
||||
selected_sheet: 0,
|
||||
sheet_states,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_range(&self) -> SelectedRange {
|
||||
let sheet = self.selected_sheet;
|
||||
let sheet_state = self.sheet_states.get(sheet as usize).unwrap();
|
||||
|
||||
SelectedRange {
|
||||
sheet,
|
||||
row: sheet_state.row,
|
||||
column: sheet_state.column,
|
||||
min_column: sheet_state.min_column,
|
||||
min_row: sheet_state.min_row,
|
||||
max_column: sheet_state.max_column,
|
||||
max_row: sheet_state.max_row,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected_sheet(&mut self, selected_sheet: u32) {
|
||||
self.selected_sheet = selected_sheet;
|
||||
}
|
||||
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
self.selected_sheet
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
let sheet = self.selected_sheet;
|
||||
let mut sheet_state = &mut self.sheet_states.get(sheet as usize).unwrap();
|
||||
sheet_state.column -= 1;
|
||||
|
||||
}
|
||||
|
||||
pub fn move_down(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_up(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_down(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_left(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub fn move_shift_right(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut file_name = "model.xlsx";
|
||||
let model = if args.len() > 1 {
|
||||
file_name = &args[1];
|
||||
if file_name.ends_with(".ic") {
|
||||
load_from_icalc(file_name).unwrap()
|
||||
} else {
|
||||
load_from_xlsx(file_name, "en", "UTC").unwrap()
|
||||
}
|
||||
} else {
|
||||
Model::new_empty(file_name, "en", "UTC").unwrap()
|
||||
};
|
||||
let mut user_model = UserModel::from_model(model);
|
||||
let mut state = ModelState::new(user_model.get_worksheets_properties().len());
|
||||
// let mut selected_sheet = 0;
|
||||
// let mut selected_row_index = 1;
|
||||
// let mut selected_column_index = 1;
|
||||
let mut minimum_row_index = 1;
|
||||
let mut minimum_column_index = 1;
|
||||
let sheet_list_width = 20;
|
||||
let column_width: u16 = 11;
|
||||
let mut cursor_mode = CursorMode::Navigate;
|
||||
let mut input_formula = Input::default();
|
||||
|
||||
let mut input_file_name: Input = file_name.into();
|
||||
|
||||
let mut popup_open = false;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
|
||||
if event::poll(timeout).expect("poll works") {
|
||||
if let CEvent::Key(key) = event::read().expect("can read events") {
|
||||
tx.send(Event::Input(key)).expect("can send events");
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate && tx.send(Event::Tick).is_ok() {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.clear()?;
|
||||
|
||||
let header_style = Style::default().fg(Color::Yellow).bg(Color::White);
|
||||
let selected_header_style = Style::default().bg(Color::Yellow).fg(Color::White);
|
||||
|
||||
let selected_cell_style = Style::default().fg(Color::Yellow).bg(Color::LightCyan);
|
||||
|
||||
let background_style = Style::default().bg(Color::Black);
|
||||
let selected_sheet_style = Style::default().bg(Color::White).fg(Color::LightMagenta);
|
||||
let non_selected_sheet_style = Style::default().fg(Color::White);
|
||||
let mut sheet_properties = user_model.get_worksheets_properties();
|
||||
loop {
|
||||
terminal.draw(|rect| {
|
||||
let size = rect.size();
|
||||
|
||||
let global_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(sheet_list_width), Constraint::Min(3)].as_ref())
|
||||
.split(size);
|
||||
|
||||
// Sheet list to the left
|
||||
let sheets = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Sheets")
|
||||
.border_type(BorderType::Plain)
|
||||
.style(background_style);
|
||||
let mut rows = vec![];
|
||||
(0..sheet_properties.len()).for_each(|sheet_index| {
|
||||
let sheet_name = &sheet_properties[sheet_index].name;
|
||||
let style = if sheet_index == state.get_selected_sheet() {
|
||||
selected_sheet_style
|
||||
} else {
|
||||
non_selected_sheet_style
|
||||
};
|
||||
rows.push(Row::new(vec![Cell::from(sheet_name.clone()).style(style)]));
|
||||
});
|
||||
let widths = &[Constraint::Length(100)];
|
||||
let sheet_list = Table::new(rows, widths).block(sheets).column_spacing(0);
|
||||
|
||||
rect.render_widget(sheet_list, global_chunks[0]);
|
||||
|
||||
// The spreadsheet is the formula bar at the top and the sheet data
|
||||
let spreadsheet_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints([Constraint::Length(1), Constraint::Min(2)].as_ref())
|
||||
.split(global_chunks[1]);
|
||||
|
||||
let spreadsheet_width = size.width - sheet_list_width;
|
||||
let spreadsheet_heigh = size.height - 1;
|
||||
let row_count = spreadsheet_heigh - 1;
|
||||
|
||||
let first_row_width: u16 = 3;
|
||||
let column_count =
|
||||
f64::ceil(((spreadsheet_width - first_row_width) as f64) / (column_width as f64))
|
||||
as i32;
|
||||
let mut rows = vec![];
|
||||
// The first row in the column headers
|
||||
let mut row = Vec::new();
|
||||
// The first cell in that row is the top left square of the spreadsheet
|
||||
row.push(Cell::from(""));
|
||||
let mut maximum_column_index = minimum_column_index + column_count - 1;
|
||||
let mut maximum_row_index = minimum_row_index + row_count - 1;
|
||||
|
||||
// We want to make sure the selected cell is visible.
|
||||
if selected_column_index > maximum_column_index {
|
||||
maximum_column_index = selected_column_index;
|
||||
minimum_column_index = maximum_column_index - column_count + 1;
|
||||
} else if selected_column_index < minimum_column_index {
|
||||
minimum_column_index = selected_column_index;
|
||||
maximum_column_index = minimum_column_index + column_count - 1;
|
||||
}
|
||||
if selected_row_index >= maximum_row_index {
|
||||
maximum_row_index = selected_row_index;
|
||||
minimum_row_index = maximum_row_index - row_count + 1;
|
||||
} else if selected_row_index < minimum_row_index {
|
||||
minimum_row_index = selected_row_index;
|
||||
maximum_row_index = minimum_row_index + row_count - 1;
|
||||
}
|
||||
for column_index in minimum_column_index..=maximum_column_index {
|
||||
let column_str = number_to_column(column_index);
|
||||
let style = if column_index == selected_column_index {
|
||||
selected_header_style
|
||||
} else {
|
||||
header_style
|
||||
};
|
||||
row.push(Cell::from(format!(" {}", column_str.unwrap())).style(style));
|
||||
}
|
||||
rows.push(Row::new(row));
|
||||
for row_index in minimum_row_index..=maximum_row_index {
|
||||
let mut row = Vec::new();
|
||||
let style = if row_index == selected_row_index {
|
||||
selected_header_style
|
||||
} else {
|
||||
header_style
|
||||
};
|
||||
row.push(Cell::from(format!("{}", row_index)).style(style));
|
||||
for column_index in minimum_column_index..=maximum_column_index {
|
||||
let value = user_model
|
||||
.get_formatted_cell_value(
|
||||
selected_sheet as u32,
|
||||
row_index as i32,
|
||||
column_index,
|
||||
)
|
||||
.unwrap();
|
||||
// let cell_style = user_model
|
||||
// .get_cell_style(selected_sheet as u32, row_index as i32, column_index)
|
||||
// .unwrap();
|
||||
let style = if selected_row_index == row_index
|
||||
&& selected_column_index == column_index
|
||||
{
|
||||
selected_cell_style
|
||||
} else {
|
||||
// let bg_color = match cell_style.fill.fg_color {
|
||||
// Some(s) => Color::from_str(&s).unwrap(),
|
||||
// None => Color::White,
|
||||
// };
|
||||
// let fg_color = match cell_style.font.color {
|
||||
// Some(s) => Color::from_str(&s).unwrap(),
|
||||
// None => Color::Black,
|
||||
// };
|
||||
let bg_color = Color::White;
|
||||
let fg_color = Color::Black;
|
||||
Style::default().fg(fg_color).bg(bg_color)
|
||||
};
|
||||
row.push(Cell::from(value.to_string()).style(style));
|
||||
}
|
||||
rows.push(Row::new(row));
|
||||
}
|
||||
let mut widths = Vec::new();
|
||||
widths.push(Constraint::Length(first_row_width));
|
||||
for _ in 0..column_count {
|
||||
widths.push(Constraint::Length(column_width));
|
||||
}
|
||||
let spreadsheet = Table::new(rows, widths)
|
||||
.block(Block::default().style(Style::default().bg(Color::Black)))
|
||||
.column_spacing(0);
|
||||
|
||||
let text = if cursor_mode != CursorMode::Input {
|
||||
user_model
|
||||
.get_cell_content(
|
||||
selected_sheet as u32,
|
||||
selected_row_index as i32,
|
||||
selected_column_index,
|
||||
)
|
||||
.unwrap()
|
||||
} else {
|
||||
input_formula.value().to_string()
|
||||
};
|
||||
let cell_address_text = format!(
|
||||
"{}{}: ",
|
||||
number_to_column(selected_column_index).unwrap(),
|
||||
selected_row_index,
|
||||
);
|
||||
let formula_bar_text = format!("{}{}", cell_address_text, text,);
|
||||
let formula_bar = Paragraph::new(vec![Line::from(vec![Span::raw(formula_bar_text)])]);
|
||||
rect.render_widget(formula_bar.block(Block::default()), spreadsheet_chunks[0]);
|
||||
rect.render_widget(spreadsheet, spreadsheet_chunks[1]);
|
||||
if cursor_mode == CursorMode::Input {
|
||||
let area = spreadsheet_chunks[0];
|
||||
rect.set_cursor(
|
||||
area.x
|
||||
+ (input_formula.visual_cursor() as u16)
|
||||
+ cell_address_text.len() as u16,
|
||||
area.y,
|
||||
)
|
||||
}
|
||||
|
||||
if popup_open {
|
||||
let area = centered_rect(60, 20, size);
|
||||
rect.render_widget(Clear, area);
|
||||
let input_text = input_file_name.value();
|
||||
let text = vec![
|
||||
Line::from(vec![input_text.fg(Color::Yellow)]),
|
||||
"".into(),
|
||||
Line::from(vec![
|
||||
"ESC".green(),
|
||||
" to abort. ".into(),
|
||||
"END".green(),
|
||||
" to quit without saving. ".into(),
|
||||
"Enter".green(),
|
||||
" to save and quit".into(),
|
||||
]),
|
||||
];
|
||||
rect.render_widget(
|
||||
Paragraph::new(text).block(Block::bordered().title("Save as")),
|
||||
area,
|
||||
);
|
||||
rect.set_cursor(
|
||||
// Put cursor past the end of the input text
|
||||
area.x + (input_file_name.visual_cursor() as u16) + 1,
|
||||
// Move one line own, from the border to the input line
|
||||
area.y + 1,
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
match cursor_mode {
|
||||
CursorMode::Popup => {
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::End => {
|
||||
terminal.clear()?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
break;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
terminal.clear()?;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
let _ = save_to_xlsx(&user_model.model, input_file_name.value());
|
||||
break;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
popup_open = false;
|
||||
cursor_mode = CursorMode::Navigate;
|
||||
}
|
||||
_ => {
|
||||
input_file_name.handle_event(&CEvent::Key(event));
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
}
|
||||
}
|
||||
CursorMode::Navigate => {
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Char('q') => {
|
||||
popup_open = true;
|
||||
cursor_mode = CursorMode::Popup;
|
||||
}
|
||||
KeyCode::Down => {
|
||||
selected_row_index += 1;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
if selected_row_index > 1 {
|
||||
selected_row_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
selected_column_index += 1;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if selected_column_index > 1 {
|
||||
selected_column_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
selected_row_index += 10;
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
if selected_row_index > 10 {
|
||||
selected_row_index -= 10;
|
||||
} else {
|
||||
selected_row_index = 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('s') => {
|
||||
selected_sheet += 1;
|
||||
if selected_sheet >= sheet_properties.len() {
|
||||
selected_sheet = 0;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') => {
|
||||
selected_sheet = selected_sheet.saturating_sub(1);
|
||||
}
|
||||
KeyCode::Char('u') => user_model.undo().unwrap(),
|
||||
KeyCode::Char('U') => user_model.redo().unwrap(),
|
||||
KeyCode::Char('c') => user_model
|
||||
.insert_column(selected_sheet as u32, selected_column_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('C') => user_model
|
||||
.delete_column(selected_sheet as u32, selected_column_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('r') => user_model
|
||||
.insert_row(selected_sheet as u32, selected_row_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('R') => user_model
|
||||
.delete_row(selected_sheet as u32, selected_row_index as i32)
|
||||
.unwrap(),
|
||||
KeyCode::Char('e') => {
|
||||
cursor_mode = CursorMode::Input;
|
||||
let input_str = user_model
|
||||
.get_cell_content(
|
||||
selected_sheet as u32,
|
||||
selected_row_index as i32,
|
||||
selected_column_index,
|
||||
)
|
||||
.unwrap();
|
||||
// .unwrap_or_default();
|
||||
input_formula = input_formula.with_value(input_str);
|
||||
}
|
||||
KeyCode::Char('+') => {
|
||||
user_model.new_sheet();
|
||||
sheet_properties = user_model.get_worksheets_properties();
|
||||
}
|
||||
_ => {
|
||||
// println!("{:?}", event);
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
}
|
||||
}
|
||||
CursorMode::Input => match rx.recv()? {
|
||||
Event::Input(event) => match event.code {
|
||||
KeyCode::Enter => {
|
||||
cursor_mode = CursorMode::Navigate;
|
||||
let value = input_formula.value().to_string();
|
||||
let sheet = selected_sheet as i32;
|
||||
let row = selected_row_index as i32;
|
||||
let column = selected_column_index;
|
||||
user_model
|
||||
.set_user_input(sheet as u32, row, column, &value)
|
||||
.unwrap();
|
||||
user_model.evaluate();
|
||||
}
|
||||
_ => {
|
||||
input_formula.handle_event(&CEvent::Key(event));
|
||||
}
|
||||
},
|
||||
Event::Tick => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::horizontal([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
194
wiki/functions.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# List of Functions implemented in IronCalc
|
||||
|
||||
* AND
|
||||
* FALSE
|
||||
* IF
|
||||
* IFERROR
|
||||
* IFNA
|
||||
* IFS
|
||||
* NOT
|
||||
* OR
|
||||
* SWITCH
|
||||
* TRUE
|
||||
* XOR
|
||||
* SIN
|
||||
* COS
|
||||
* TAN
|
||||
* ASIN
|
||||
* ACOS
|
||||
* ATAN
|
||||
* SINH
|
||||
* COSH
|
||||
* TANH
|
||||
* ASINH
|
||||
* ACOSH
|
||||
* ATANH
|
||||
* ABS
|
||||
* PI
|
||||
* SQRT
|
||||
* SQRTPI
|
||||
* ATAN2
|
||||
* POWER
|
||||
* MAX
|
||||
* MIN
|
||||
* PRODUCT
|
||||
* RAND
|
||||
* RANDBETWEEN
|
||||
* ROUND
|
||||
* ROUNDDOWN
|
||||
* ROUNDUP
|
||||
* SUM
|
||||
* SUMIF
|
||||
* SUMIFS
|
||||
* CHOOSE
|
||||
* COLUMN
|
||||
* COLUMNS
|
||||
* INDEX
|
||||
* INDIRECT
|
||||
* HLOOKUP
|
||||
* LOOKUP
|
||||
* MATCH
|
||||
* OFFSET
|
||||
* ROW
|
||||
* ROWS
|
||||
* VLOOKUP
|
||||
* XLOOKUP
|
||||
* CONCATENATE
|
||||
* EXACT
|
||||
* VALUE
|
||||
* T
|
||||
* VALUETOTEXT
|
||||
* CONCAT
|
||||
* FIND
|
||||
* LEFT
|
||||
* LEN
|
||||
* LOWER
|
||||
* MID
|
||||
* RIGHT
|
||||
* SEARCH
|
||||
* TEXT
|
||||
* TRIM
|
||||
* UPPER
|
||||
* ISNUMBER
|
||||
* ISNONTEXT
|
||||
* ISTEXT
|
||||
* ISLOGICAL
|
||||
* ISBLANK
|
||||
* ISERR
|
||||
* ISERROR
|
||||
* ISNA
|
||||
* NA
|
||||
* ISREF
|
||||
* ISODD
|
||||
* ISEVEN
|
||||
* ERROR.TYPE
|
||||
* ISFORMULA
|
||||
* TYPE
|
||||
* SHEET
|
||||
* AVERAGE
|
||||
* AVERAGEA
|
||||
* AVERAGEIF
|
||||
* AVERAGEIFS
|
||||
* COUNT
|
||||
* COUNTA
|
||||
* COUNTBLANK
|
||||
* COUNTIF
|
||||
* COUNTIFS
|
||||
* MAXIFS
|
||||
* MINIFS
|
||||
* YEAR
|
||||
* DAY
|
||||
* MONTH
|
||||
* EOMONTH
|
||||
* DATE
|
||||
* EDATE
|
||||
* TODAY
|
||||
* NOW
|
||||
* PMT
|
||||
* PV
|
||||
* RATE
|
||||
* NPER
|
||||
* FV
|
||||
* PPMT
|
||||
* IPMT
|
||||
* NPV
|
||||
* MIRR
|
||||
* IRR
|
||||
* XIRR
|
||||
* XNPV
|
||||
* REPT
|
||||
* TEXTAFTER
|
||||
* TEXTBEFORE
|
||||
* TEXTJOIN
|
||||
* SUBSTITUTE
|
||||
* ISPMT
|
||||
* RRI
|
||||
* SLN
|
||||
* SYD
|
||||
* NOMINAL
|
||||
* EFFECT
|
||||
* PDURATION
|
||||
* TBILLYIELD
|
||||
* TBILLPRICE
|
||||
* TBILLEQ
|
||||
* DOLLARDE
|
||||
* DOLLARFR
|
||||
* DDB
|
||||
* DB
|
||||
* CUMPRINC
|
||||
* CUMIPMT
|
||||
* BESSELI
|
||||
* BESSELJ
|
||||
* BESSELK
|
||||
* BESSELY
|
||||
* ERF
|
||||
* ERF.PRECISE
|
||||
* ERFC
|
||||
* ERFC.PRECISE
|
||||
* BIN2DEC
|
||||
* BIN2HEX
|
||||
* BIN2OCT
|
||||
* DEC2BIN
|
||||
* DEC2HEX
|
||||
* DEC2OCT
|
||||
* HEX2BIN
|
||||
* HEX2DEC
|
||||
* HEX2OCT
|
||||
* OCT2BIN
|
||||
* OCT2DEC
|
||||
* OCT2HEX
|
||||
* BITAND
|
||||
* BITLSHIFT
|
||||
* BITOR
|
||||
* BITRSHIFT
|
||||
* BITXOR
|
||||
* COMPLEX
|
||||
* 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
|
||||
* CONVERT
|
||||
* DELTA
|
||||
* GESTEP
|
||||
* SUBTOTAL
|
||||
@@ -12,8 +12,8 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
zip = "0.5"
|
||||
roxmltree = "0.13.0"
|
||||
zip = "0.6"
|
||||
roxmltree = "0.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
@@ -21,8 +21,9 @@ thiserror = "1.0"
|
||||
# the inicated version from crates.io when published.
|
||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
||||
ironcalc_base = { path = "../base", version = "0.1.0" }
|
||||
itertools = "0.10.5"
|
||||
itertools = "0.12"
|
||||
chrono = "0.4"
|
||||
bitcode = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
uuid = { version = "1.2.2", features = ["serde", "v4"] }
|
||||
@@ -34,3 +35,8 @@ path = "src/lib.rs"
|
||||
[[bin]]
|
||||
name = "test"
|
||||
path = "src/bin/test.rs"
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "documentation"
|
||||
path = "src/bin/documentation.rs"
|
||||
@@ -1,5 +1,5 @@
|
||||
use ironcalc::{
|
||||
base::{expressions::utils::number_to_column, model::Model},
|
||||
base::{expressions::utils::number_to_column, Model},
|
||||
export::save_to_xlsx,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc::{base::model::Model, export::save_to_xlsx};
|
||||
use ironcalc::{base::Model, export::save_to_xlsx};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("hello_styles", "en", "UTC")?;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ironcalc::{base::model::Model, export::save_to_xlsx};
|
||||
use ironcalc::{base::Model, export::save_to_xlsx};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut model = Model::new_empty("widths-and-heights", "en", "UTC")?;
|
||||
@@ -6,7 +6,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (sheet, row, column) = (0, 5, 3);
|
||||
// Make the first column 4 times as width
|
||||
let worksheet = model.workbook.worksheet_mut(sheet)?;
|
||||
let column_width = worksheet.column_width(column)? * 4.0;
|
||||
let column_width = worksheet.get_column_width(column)? * 4.0;
|
||||
worksheet.set_column_width(column, column_width)?;
|
||||
|
||||
// and the first row twice as high.
|
||||
|
||||