Compare commits
25 Commits
plot-popup
...
53dcf2ffc9
| Author | SHA1 | Date | |
|---|---|---|---|
| 53dcf2ffc9 | |||
| 86756a94ef | |||
| d242e1af21 | |||
| abffe6073f | |||
| d983995e8f | |||
| 045d1d6554 | |||
| 9691268d3d | |||
| f3356c1398 | |||
| b3b2c59a36 | |||
| 077b53f6ff | |||
| 0c78e7834b | |||
| 052828c89c | |||
| 626b8ff4eb | |||
| 69c7ebf24b | |||
| 8d3f003427 | |||
| 1923bbe674 | |||
| 2076c42c80 | |||
| 91fb658f65 | |||
| d82a3d263e | |||
| 4452d3210e | |||
| da5e4921d8 | |||
| b5de08300a | |||
| 4ae7aec66e | |||
| 3ff9e8683c | |||
| 0c2588a299 |
21
.gitea/workflows/test.yaml
Normal file
21
.gitea/workflows/test.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Test Rust project
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
rust: [stable]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: https://git.oliveratkinson.net/Oliver/setup-rust-action@master
|
||||||
|
with:
|
||||||
|
rust-version: ${{ matrix.rust }}
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
- name: Clippy
|
||||||
|
run: cargo clippy
|
||||||
|
|
||||||
|
|
||||||
39
README.md
39
README.md
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
*New* Spreadsheet Calculator Improved
|
*New* Spreadsheet Calculator Improved
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keybinds and not many features.
|
||||||
|
|
||||||
|
[Checkout a demo file](readme/demo.nscim), download, then open with `neoscim demo.nscim`.
|
||||||
|
|
||||||
## Keybinds
|
## Keybinds
|
||||||
|
|
||||||
### Normal mode
|
### Normal mode
|
||||||
@@ -14,8 +18,13 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
| `k` | Move up |
|
| `k` | Move up |
|
||||||
| `h` | Move left |
|
| `h` | Move left |
|
||||||
| `l` | Move right |
|
| `l` | Move right |
|
||||||
| `0` | Go to beginning of row |
|
| `0` | Go to beginning of the row |
|
||||||
| `gg` | Go to beginning of column |
|
| `g0` | Go to beginning of the visual row |
|
||||||
|
| `$` | Go to end of the row |
|
||||||
|
| `g$` | Go to end of the visual row |
|
||||||
|
| `gg` | Go to beginning of the column |
|
||||||
|
| `G` | Go to end of column |
|
||||||
|
| `gG` | Go to end of the the visual column |
|
||||||
| `i`/`a` | Enter insert mode on current cell |
|
| `i`/`a` | Enter insert mode on current cell |
|
||||||
| `r` | Enter insert mode on current cell, deleting contents |
|
| `r` | Enter insert mode on current cell, deleting contents |
|
||||||
| `v` | Enter visual mode |
|
| `v` | Enter visual mode |
|
||||||
@@ -51,6 +60,16 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
| `:q` | Quit program |
|
| `:q` | Quit program |
|
||||||
| `:q!` | Quit program, even if the file isn't saved |
|
| `:q!` | Quit program, even if the file isn't saved |
|
||||||
|
|
||||||
|
#### Visual mode commands
|
||||||
|
|
||||||
|
These commands operate on selections.
|
||||||
|
|
||||||
|
| Commmand | Description |
|
||||||
|
| - | - |
|
||||||
|
| `:plot` | Plots the current selection to `plot.png` using gnuplot |
|
||||||
|
| `:export <filename>` | Exports the current selection to a new file |
|
||||||
|
| `:fill <value>` | (Aliased as `f`) Fill the selection with `<value>`, special variables `x`,`y`,`xi`, and `yi` are avaliable. Variables `x` and `y` are the global coordinates of the cell, and `xi` and `yi` are the local coordinates of the selection. |
|
||||||
|
|
||||||
## Math / Functions
|
## Math / Functions
|
||||||
|
|
||||||
### Math
|
### Math
|
||||||
@@ -68,11 +87,11 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
|
|
||||||
| Identifier | Arg Qty | Argument Types | Description |
|
| Identifier | Arg Qty | Argument Types | Description |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| `avg` | >= 1 | Numeric | Returns the average of the arguments |
|
| `avg` | ≥ 1 | Numeric | Returns the average of the arguments |
|
||||||
| `sum` | >= 1 | Numbers | Returns the sum of the arguments |
|
| `sum` | ≥ 1 | Numbers | Returns the sum of the arguments |
|
||||||
| `xlookup` | 3 | Range, Number/String, Range | Searches for the number/string in the first range, returning the item at the same index in the second range |
|
| `xlookup` | 3 | Number/String, Range, Range | Searches for the number/string in the first range, returning the item at the same index in the second range |
|
||||||
| `min` | >= 1 | Numeric | Returns the minimum of the arguments |
|
| `min` | ≥ 1 | Numeric | Returns the minimum of the arguments |
|
||||||
| `max` | >= 1 | Numeric | Returns the maximum of the arguments |
|
| `max` | ≥ 1 | Numeric | Returns the maximum of the arguments |
|
||||||
| `len` | 1 | String/Tuple | Returns the character length of a string, or the amount of elements in a tuple (not recursively) |
|
| `len` | 1 | String/Tuple | Returns the character length of a string, or the amount of elements in a tuple (not recursively) |
|
||||||
| `floor` | 1 | Numeric | Returns the largest integer less than or equal to a number |
|
| `floor` | 1 | Numeric | Returns the largest integer less than or equal to a number |
|
||||||
| `round` | 1 | Numeric | Returns the nearest integer to a number. Rounds half-way cases away from 0.0 |
|
| `round` | 1 | Numeric | Returns the nearest integer to a number. Rounds half-way cases away from 0.0 |
|
||||||
@@ -112,7 +131,7 @@ Based loosely off sc-im (spreadsheet calculator improvised), which has dumb keyb
|
|||||||
| `str::to_lowercase` | 1 | String | Returns the lower-case version of the string |
|
| `str::to_lowercase` | 1 | String | Returns the lower-case version of the string |
|
||||||
| `str::to_uppercase` | 1 | String | Returns the upper-case version of the string |
|
| `str::to_uppercase` | 1 | String | Returns the upper-case version of the string |
|
||||||
| `str::trim` | 1 | String | Strips whitespace from the start and the end of the string |
|
| `str::trim` | 1 | String | Strips whitespace from the start and the end of the string |
|
||||||
| `str::from` | >= 0 | Any | Returns passed value as string |
|
| `str::from` | ≥ 0 | Any | Returns passed value as string |
|
||||||
| `str::substring` | 3 | String, Int, Int | Returns a substring of the first argument, starting at the second argument and ending at the third argument. If the last argument is omitted, the substring extends to the end of the string |
|
| `str::substring` | 3 | String, Int, Int | Returns a substring of the first argument, starting at the second argument and ending at the third argument. If the last argument is omitted, the substring extends to the end of the string |
|
||||||
| `bitand` | 2 | Int | Computes the bitwise and of the given integers |
|
| `bitand` | 2 | Int | Computes the bitwise and of the given integers |
|
||||||
| `bitor` | 2 | Int | Computes the bitwise or of the given integers |
|
| `bitor` | 2 | Int | Computes the bitwise or of the given integers |
|
||||||
@@ -132,8 +151,8 @@ If the cell can be a number (parsed into a float) it will be, if not, then if th
|
|||||||
|
|
||||||
* Cell references move with copy / paste
|
* Cell references move with copy / paste
|
||||||
* =A0: Will translate
|
* =A0: Will translate
|
||||||
* =$A0: Will translate Y only
|
* =$A0: Will translate Y only (numbers)
|
||||||
* =$A0: Will translate X only
|
* =A$0: Will translate X only (letters)
|
||||||
* =$A$0: No translation
|
* =$A$0: No translation
|
||||||
|
|
||||||
* Keybinds are closer to vim keying (i/a start inserting into a cell)
|
* Keybinds are closer to vim keying (i/a start inserting into a cell)
|
||||||
|
|||||||
21
readme/demo.nscim
Normal file
21
readme/demo.nscim
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
,,,
|
||||||
|
Most of vim's movement works here as well,,,
|
||||||
|
,,,
|
||||||
|
,i,,
|
||||||
|
j,Movement,l,
|
||||||
|
,j,,
|
||||||
|
,,,
|
||||||
|
,"insert text with r, i, or a",,
|
||||||
|
,,,
|
||||||
|
1,,,
|
||||||
|
1,Text can overflow cells,,
|
||||||
|
1,But may get cut off if it's too long,,10
|
||||||
|
1,,,If it reaches another cell
|
||||||
|
1,,,
|
||||||
|
Total:,=sum(A:A),Sum A:A,
|
||||||
|
,=math::log2(B14),Do math on the output of another function,
|
||||||
|
,,,
|
||||||
|
,,,
|
||||||
|
,,11,Number.
|
||||||
|
,,=C18+1,Referencing the number and adding 1
|
||||||
|
,,,Copying the cell down will translate the reference
|
||||||
BIN
readme/screenshot.png
Normal file
BIN
readme/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -18,7 +18,7 @@ use ratatui::{
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
clipboard::Clipboard,
|
clipboard::Clipboard,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{calc::Grid, cell::CellType},
|
logic::{calc::{Grid, get_header_size}, cell::CellType},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
screen::ScreenSpace,
|
screen::ScreenSpace,
|
||||||
};
|
};
|
||||||
@@ -63,8 +63,12 @@ impl Widget for &App {
|
|||||||
for x in 0..x_max {
|
for x in 0..x_max {
|
||||||
for y in 0..y_max {
|
for y in 0..y_max {
|
||||||
let mut display = String::new();
|
let mut display = String::new();
|
||||||
let mut style = Style::new().fg(Color::White);
|
let mut style = Style::new();
|
||||||
|
|
||||||
|
// Custom width for the header of each row
|
||||||
|
let row_header_width = get_header_size() as u16;
|
||||||
|
// ^^ Feels like it oculd be static but evaluating string lens doesn't work at
|
||||||
|
// compile time. Thus cannot be static.
|
||||||
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
|
let cell_width = self.screen.get_cell_width(&self.vars) as u16;
|
||||||
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
|
let cell_height = self.screen.get_cell_height(&self.vars) as u16;
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ impl Widget for &App {
|
|||||||
let mut x_idx: usize = 0;
|
let mut x_idx: usize = 0;
|
||||||
let mut y_idx: usize = 0;
|
let mut y_idx: usize = 0;
|
||||||
if x != 0 {
|
if x != 0 {
|
||||||
x_idx = x as usize - 1 + self.screen.scroll_x();
|
x_idx = x as usize -1 + self.screen.scroll_x();
|
||||||
}
|
}
|
||||||
if y != 0 {
|
if y != 0 {
|
||||||
y_idx = y as usize - 1 + self.screen.scroll_y();
|
y_idx = y as usize - 1 + self.screen.scroll_y();
|
||||||
@@ -86,6 +90,15 @@ impl Widget for &App {
|
|||||||
let mut should_render = true;
|
let mut should_render = true;
|
||||||
let mut suggest_upper_bound = None;
|
let mut suggest_upper_bound = None;
|
||||||
|
|
||||||
|
/// Center the text "99 " -> " 99 "
|
||||||
|
fn center_text(text: &str, avaliable_space: i32) -> String {
|
||||||
|
let margin = avaliable_space - text.len() as i32;
|
||||||
|
let margin = margin/2;
|
||||||
|
let l_margin = (0..margin).into_iter().map(|_| ' ').collect::<String>();
|
||||||
|
let r_margin = (0..(margin-(l_margin.len() as i32))).into_iter().map(|_| ' ').collect::<String>();
|
||||||
|
format!("{l_margin}{text}{r_margin}")
|
||||||
|
}
|
||||||
|
|
||||||
match (x == 0, y == 0) {
|
match (x == 0, y == 0) {
|
||||||
// 0,0 vi mode
|
// 0,0 vi mode
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
@@ -94,7 +107,7 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
// row names
|
// row names
|
||||||
(true, false) => {
|
(true, false) => {
|
||||||
display = y_idx.to_string();
|
display = center_text(&y_idx.to_string(), row_header_width as i32);
|
||||||
|
|
||||||
let bg = if y_idx == self.grid.cursor().1 {
|
let bg = if y_idx == self.grid.cursor().1 {
|
||||||
Color::DarkGray
|
Color::DarkGray
|
||||||
@@ -107,7 +120,7 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
// column names
|
// column names
|
||||||
(false, true) => {
|
(false, true) => {
|
||||||
display = Grid::num_to_char(x_idx);
|
display = center_text(&Grid::num_to_char(x_idx), cell_width as i32);
|
||||||
|
|
||||||
let bg = if x_idx == self.grid.cursor().0 {
|
let bg = if x_idx == self.grid.cursor().0 {
|
||||||
Color::DarkGray
|
Color::DarkGray
|
||||||
@@ -123,6 +136,7 @@ impl Widget for &App {
|
|||||||
(false, false) => {
|
(false, false) => {
|
||||||
match self.grid.get_cell_raw(x_idx, y_idx) {
|
match self.grid.get_cell_raw(x_idx, y_idx) {
|
||||||
Some(cell) => {
|
Some(cell) => {
|
||||||
|
// Render in different colors based on type of contents
|
||||||
match cell {
|
match cell {
|
||||||
CellType::Number(c) => display = c.to_string(),
|
CellType::Number(c) => display = c.to_string(),
|
||||||
CellType::String(s) => {
|
CellType::String(s) => {
|
||||||
@@ -134,6 +148,9 @@ impl Widget for &App {
|
|||||||
Ok(val) => {
|
Ok(val) => {
|
||||||
display = val.to_string();
|
display = val.to_string();
|
||||||
style = Style::new()
|
style = Style::new()
|
||||||
|
.fg(Color::White)
|
||||||
|
// TODO This breaks dumb terminals like the windows
|
||||||
|
// terminal
|
||||||
.underline_color(Color::DarkGray)
|
.underline_color(Color::DarkGray)
|
||||||
.add_modifier(Modifier::UNDERLINED);
|
.add_modifier(Modifier::UNDERLINED);
|
||||||
}
|
}
|
||||||
@@ -149,6 +166,7 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow for text in one cell to visually overflow into empty cells
|
||||||
suggest_upper_bound = Some(display.len() as u16);
|
suggest_upper_bound = Some(display.len() as u16);
|
||||||
// check for cells to the right, see if we should truncate the cell width
|
// check for cells to the right, see if we should truncate the cell width
|
||||||
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
|
for i in 1..(display.len() as f32 / cell_width as f32).ceil() as usize {
|
||||||
@@ -165,11 +183,14 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Don't render blank cells
|
||||||
None => should_render = false,
|
None => should_render = false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_selected(x.into(), y.into()) {
|
if is_selected(x.into(), y.into()) {
|
||||||
style = style.bg(Color::Blue);
|
style = style.bg(Color::Blue);
|
||||||
|
// Make it so that cells render when selected. This fixes issue #32
|
||||||
|
should_render = true;
|
||||||
}
|
}
|
||||||
if (x_idx, y_idx) == self.grid.cursor() {
|
if (x_idx, y_idx) == self.grid.cursor() {
|
||||||
should_render = true;
|
should_render = true;
|
||||||
@@ -181,11 +202,21 @@ impl Widget for &App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_render {
|
if should_render {
|
||||||
let x_off = area.x + (x * cell_width);
|
let mut x_off = area.x + (x * cell_width);
|
||||||
let y_off = area.y + (y * cell_height);
|
let y_off = area.y + (y * cell_height);
|
||||||
|
|
||||||
let area = if let Some(suggestion) = suggest_upper_bound {
|
// Adjust for the fact that the first column
|
||||||
|
// is smaller, since it is just headers
|
||||||
|
if x > 0 {
|
||||||
|
x_off = x_off - (cell_width - row_header_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the row header column
|
||||||
|
let area = if x==0 && y != 0 {
|
||||||
|
Rect::new(x_off, y_off, row_header_width, cell_height)
|
||||||
|
} else if let Some(suggestion) = suggest_upper_bound {
|
||||||
let max_available_width = area.width - x_off;
|
let max_available_width = area.width - x_off;
|
||||||
// draw the biggest cell possible, without going OOB off the screen
|
// draw the biggest cell possible, without going OOB off the screen
|
||||||
let width = min(max_available_width, suggestion as u16);
|
let width = min(max_available_width, suggestion as u16);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ impl Clipboard {
|
|||||||
// cursor
|
// cursor
|
||||||
let (cx, cy) = into.cursor();
|
let (cx, cy) = into.cursor();
|
||||||
|
|
||||||
|
// iterate thru the clipbaord's cells
|
||||||
for (x, row) in self.clipboard.iter().enumerate() {
|
for (x, row) in self.clipboard.iter().enumerate() {
|
||||||
for (y, cell) in row.iter().enumerate() {
|
for (y, cell) in row.iter().enumerate() {
|
||||||
let idx = (x + cx, y + cy);
|
let idx = (x + cx, y + cy);
|
||||||
@@ -58,7 +59,7 @@ impl Clipboard {
|
|||||||
let trans = cell.translate_cell(self.source_cell, into.cursor());
|
let trans = cell.translate_cell(self.source_cell, into.cursor());
|
||||||
into.set_cell_raw(idx, Some(trans));
|
into.set_cell_raw(idx, Some(trans));
|
||||||
} else {
|
} else {
|
||||||
// cell doesn't exist, no need to translate
|
// The cell at this location doesn't exist (empty)
|
||||||
into.set_cell_raw::<CellType>(idx, None);
|
into.set_cell_raw::<CellType>(idx, None);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -349,4 +350,49 @@ fn copy_paste_y_locked_var() {
|
|||||||
Mode::process_key(&mut app, 'p');
|
Mode::process_key(&mut app, 'p');
|
||||||
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
|
let c = app.grid.get_cell("B2").as_ref().expect("Just set it");
|
||||||
assert_eq!(c.to_string(), "=B$0");
|
assert_eq!(c.to_string(), "=B$0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_var_in_function() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.grid.set_cell("A0", 4.to_string());
|
||||||
|
Mode::process_key(&mut app, 'j');
|
||||||
|
|
||||||
|
app.grid.set_cell("A1", "=math::log2(A0)".to_string());
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
Mode::process_key(&mut app, 'j');
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
let a = app.grid.get_cell("A2").as_ref().expect("Should've been set by paste");
|
||||||
|
assert_eq!(a.to_string(), "=math::log2(A1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy_paste_range_in_function() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.grid.set_cell("A0", 1.to_string());
|
||||||
|
app.grid.set_cell("A1", 1.to_string());
|
||||||
|
app.grid.set_cell("A2", 1.to_string());
|
||||||
|
|
||||||
|
app.grid.set_cell("B0", "=sum(A:A)".to_string());
|
||||||
|
app.grid.mv_cursor_to(1, 0);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
Mode::process_key(&mut app, 'l');
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
let a = app.grid.get_cell("C0").as_ref().expect("Should've been set by paste");
|
||||||
|
assert_eq!(a.to_string(), "=sum(B:B)");
|
||||||
|
|
||||||
|
// now copy the range the other direction
|
||||||
|
app.grid.mv_cursor_to(2, 0);
|
||||||
|
app.mode = super::mode::Mode::Chord(Chord::new('y'));
|
||||||
|
Mode::process_key(&mut app, 'y');
|
||||||
|
app.grid.mv_cursor_to(1, 1);
|
||||||
|
Mode::process_key(&mut app, 'p');
|
||||||
|
|
||||||
|
let a = app.grid.get_cell("B1").as_ref().expect("Should've been set by paste");
|
||||||
|
assert_eq!(a.to_string(), "=sum(A:A)");
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,32 +2,134 @@ use std::{
|
|||||||
cmp::{max, min},
|
cmp::{max, min},
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
path::PathBuf
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use evalexpr::*;
|
use evalexpr::*;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
logic::{
|
logic::{
|
||||||
cell::{CSV_DELIMITER, CellType},
|
calc::internal::CellGrid, cell::{CSV_DELIMITER, CellType}, ctx
|
||||||
ctx,
|
}
|
||||||
}, mode::Mode,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::app::app::App;
|
use crate::app::app::App;
|
||||||
|
#[cfg(test)]
|
||||||
|
use crate::app::mode::Mode;
|
||||||
|
|
||||||
pub const LEN: usize = 1000;
|
pub fn get_header_size() -> usize {
|
||||||
|
let row_header_width = LEN.to_string().len();
|
||||||
|
row_header_width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const LEN: usize = 1001;
|
||||||
pub const CSV_EXT: &str = "csv";
|
pub const CSV_EXT: &str = "csv";
|
||||||
pub const CUSTOM_EXT: &str = "nscim";
|
pub const CUSTOM_EXT: &str = "nscim";
|
||||||
|
|
||||||
|
mod internal {
|
||||||
|
use crate::app::logic::{calc::LEN, cell::CellType};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CellGrid {
|
||||||
|
// a b c ...
|
||||||
|
// 0
|
||||||
|
// 1
|
||||||
|
// 2
|
||||||
|
// ...
|
||||||
|
cells: Vec<Vec<Option<CellType>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellGrid {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut a = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
let mut b = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
b.push(None)
|
||||||
|
}
|
||||||
|
a.push(b)
|
||||||
|
}
|
||||||
|
Self { cells: a }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_row(&mut self, y: usize) {
|
||||||
|
for x in 0..LEN {
|
||||||
|
self.cells[x].insert(y, None);
|
||||||
|
self.cells[x].pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_column(&mut self, x: usize) {
|
||||||
|
let mut v = Vec::with_capacity(LEN);
|
||||||
|
for _ in 0..LEN {
|
||||||
|
v.push(None);
|
||||||
|
}
|
||||||
|
// let clone = self.grid_history[self.current_grid].clone();
|
||||||
|
self.cells.insert(x, v);
|
||||||
|
// keep the grid LEN
|
||||||
|
self.cells.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cell_raw(&self, x: usize, y: usize) -> &Option<CellType> {
|
||||||
|
if x >= LEN || y >= LEN {
|
||||||
|
return &None;
|
||||||
|
}
|
||||||
|
&self.cells[x][y]
|
||||||
|
}
|
||||||
|
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
|
||||||
|
// TODO check oob
|
||||||
|
self.cells[x][y] = val.map(|v| v.into());
|
||||||
|
}
|
||||||
|
/// Iterate over the entire grid and see where
|
||||||
|
/// the farthest modified cell is.
|
||||||
|
#[must_use]
|
||||||
|
pub fn max(&self) -> (usize, usize) {
|
||||||
|
let mut max_x = 0;
|
||||||
|
let mut max_y = 0;
|
||||||
|
|
||||||
|
for (xi, x) in self.cells.iter().enumerate() {
|
||||||
|
for (yi, cell) in x.iter().enumerate() {
|
||||||
|
if cell.is_some() {
|
||||||
|
if yi > max_y {
|
||||||
|
max_y = yi
|
||||||
|
}
|
||||||
|
if xi > max_x {
|
||||||
|
max_x = xi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(max_x, max_y)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn max_y_at_x(&self, x: usize) -> usize {
|
||||||
|
let mut max_y = 0;
|
||||||
|
if let Some(col) = self.cells.get(x) {
|
||||||
|
// we could do fancy things like .take_while(not null) but then
|
||||||
|
// we would have to deal with empty cells and stuff, which sounds
|
||||||
|
// boring. This will be fast "enough", considering the grid is
|
||||||
|
// probably only like 1k cells
|
||||||
|
for (yi, cell) in col.iter().enumerate() {
|
||||||
|
if cell.is_some() {
|
||||||
|
if yi > max_y {
|
||||||
|
max_y = yi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max_y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
// a b c ...
|
/// Which grid in history are we currently on
|
||||||
// 0
|
current_grid: usize,
|
||||||
// 1
|
/// An array of grids, thru history
|
||||||
// 2
|
grid_history: Vec<CellGrid>,
|
||||||
// ...
|
|
||||||
cells: Vec<Vec<Option<CellType>>>,
|
|
||||||
/// (X, Y)
|
/// (X, Y)
|
||||||
selected_cell: (usize, usize),
|
selected_cell: (usize, usize),
|
||||||
/// Have unsaved modifications been made?
|
/// Have unsaved modifications been made?
|
||||||
@@ -42,20 +144,9 @@ impl std::fmt::Debug for Grid {
|
|||||||
|
|
||||||
impl Grid {
|
impl Grid {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut a = Vec::with_capacity(LEN);
|
let x = CellGrid::new();
|
||||||
for _ in 0..LEN {
|
|
||||||
let mut b = Vec::with_capacity(LEN);
|
|
||||||
for _ in 0..LEN {
|
|
||||||
b.push(None)
|
|
||||||
}
|
|
||||||
a.push(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self { current_grid: 0, grid_history: vec![x], selected_cell: (0, 0), dirty: false }
|
||||||
cells: a,
|
|
||||||
selected_cell: (0, 0),
|
|
||||||
dirty: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
|
pub fn new_from_file(file: &mut File) -> std::io::Result<Self> {
|
||||||
@@ -81,7 +172,7 @@ impl Grid {
|
|||||||
/// Save file to `path` as a csv. Path with have `csv` appended to it if it
|
/// Save file to `path` as a csv. Path with have `csv` appended to it if it
|
||||||
/// does not already have the extension.
|
/// does not already have the extension.
|
||||||
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
|
pub fn save_to(&mut self, path: impl Into<PathBuf>) -> std::io::Result<()> {
|
||||||
let path = path.into();
|
let mut path = path.into();
|
||||||
|
|
||||||
let resolve_values;
|
let resolve_values;
|
||||||
|
|
||||||
@@ -94,30 +185,28 @@ impl Grid {
|
|||||||
resolve_values = false;
|
resolve_values = false;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
resolve_values = false;
|
// File as an extension but isn't ours.
|
||||||
// path.add_extension(CUSTOM_EXT);
|
// Save as csv-like
|
||||||
|
resolve_values = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
|
// File has no extension. Save it as our file type
|
||||||
resolve_values = false;
|
resolve_values = false;
|
||||||
// path.add_extension(CUSTOM_EXT);
|
path.add_extension(CUSTOM_EXT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
let mut f = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
||||||
let (mx, my) = self.max();
|
let (mx, my) = self.get_grid().max();
|
||||||
for y in 0..=my {
|
for y in 0..=my {
|
||||||
for x in 0..=mx {
|
for x in 0..=mx {
|
||||||
let cell = &self.cells[x][y];
|
let cell = &self.get_grid().get_cell_raw(x, y);
|
||||||
|
|
||||||
// newline after the cell, because it's end of line.
|
// newline after the cell, because it's end of line.
|
||||||
// else, just put a comma after the cell.
|
// else, just put a comma after the cell.
|
||||||
let is_last = x==mx;
|
let is_last = x == mx;
|
||||||
let delim = if is_last {
|
let delim = if is_last { '\n' } else { CSV_DELIMITER };
|
||||||
'\n'
|
|
||||||
} else {
|
|
||||||
CSV_DELIMITER
|
|
||||||
};
|
|
||||||
|
|
||||||
let data = if let Some(cell) = cell {
|
let data = if let Some(cell) = cell {
|
||||||
if let Ok(val) = self.evaluate(&cell.to_string())
|
if let Ok(val) = self.evaluate(&cell.to_string())
|
||||||
@@ -139,6 +228,40 @@ impl Grid {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_grid<'a>(&'a self) -> &'a CellGrid {
|
||||||
|
&self.grid_history[self.current_grid]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_grid_mut<'a>(&'a mut self) -> &'a mut CellGrid {
|
||||||
|
&mut self.grid_history[self.current_grid]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo(&mut self) {
|
||||||
|
self.current_grid.saturating_sub(1);
|
||||||
|
}
|
||||||
|
fn redo(&mut self) {
|
||||||
|
self.current_grid += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transact_on_grid<F>(&mut self, mut action: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&mut CellGrid) -> (),
|
||||||
|
{
|
||||||
|
// push on a new reality
|
||||||
|
let new = self.get_grid().clone();
|
||||||
|
self.grid_history.push(new);
|
||||||
|
self.current_grid += 1;
|
||||||
|
|
||||||
|
// delete the other fork of the history
|
||||||
|
for i in self.current_grid+1..self.grid_history.len() {
|
||||||
|
self.grid_history.remove(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
action(&mut self.grid_history[self.current_grid]);
|
||||||
|
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn needs_to_be_saved(&self) -> bool {
|
pub fn needs_to_be_saved(&self) -> bool {
|
||||||
self.dirty
|
self.dirty
|
||||||
}
|
}
|
||||||
@@ -245,101 +368,59 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
|
pub fn insert_row_above(&mut self, (_x, insertion_y): (usize, usize)) {
|
||||||
for x in 0..LEN {
|
self.transact_on_grid(|grid: &mut CellGrid| {
|
||||||
self.cells[x].insert(insertion_y, None);
|
grid.insert_row(insertion_y);
|
||||||
self.cells[x].pop();
|
for x in 0..LEN {
|
||||||
for y in 0..LEN {
|
for y in 0..LEN {
|
||||||
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
|
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
|
||||||
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
|
f.custom_translate_cell((0, 0), (0, 1), |rolling, old, new| {
|
||||||
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
|
if let Some((_, arg_y)) = Grid::parse_to_idx(old) {
|
||||||
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
|
if arg_y < insertion_y { rolling.to_owned() } else { rolling.replace(old, new) }
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
unimplemented!("Invalid variable wanted to be translated")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
grid.set_cell_raw((x, y), Some(cell));
|
||||||
}
|
}
|
||||||
)}) {
|
|
||||||
self.set_cell_raw((x,y), Some(cell));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
|
||||||
self.insert_row_above((x,y+1));
|
|
||||||
}
|
|
||||||
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
|
||||||
let mut v = Vec::with_capacity(LEN);
|
|
||||||
for _ in 0..LEN {
|
|
||||||
v.push(None);
|
|
||||||
}
|
|
||||||
self.cells.insert(insertion_x, v);
|
|
||||||
// keep the grid LEN
|
|
||||||
self.cells.pop();
|
|
||||||
for x in 0..LEN {
|
|
||||||
for y in 0..LEN {
|
|
||||||
if let Some(cell) = self.get_cell_raw(x, y).as_ref().map(|f| {
|
|
||||||
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
|
|
||||||
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
|
||||||
// add 1 because of the insertion
|
|
||||||
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
|
||||||
} else {
|
|
||||||
unimplemented!("Invalid variable wanted to be translated")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}) {
|
|
||||||
self.set_cell_raw((x, y), Some(cell));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_row_below(&mut self, (x, y): (usize, usize)) {
|
||||||
|
self.insert_row_above((x, y + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert_column_before(&mut self, (insertion_x, _y): (usize, usize)) {
|
||||||
|
self.transact_on_grid(|grid| {
|
||||||
|
grid.insert_column(insertion_x);
|
||||||
|
for x in 0..LEN {
|
||||||
|
for y in 0..LEN {
|
||||||
|
if let Some(cell) = grid.get_cell_raw(x, y).as_ref().map(|f| {
|
||||||
|
f.custom_translate_cell((0, 0), (1, 0), |rolling, old, new| {
|
||||||
|
if let Some((arg_x, _)) = Grid::parse_to_idx(old) {
|
||||||
|
// add 1 because of the insertion
|
||||||
|
if arg_x < insertion_x { rolling.to_owned() } else { rolling.replace(old, new) }
|
||||||
|
} else {
|
||||||
|
unimplemented!("Invalid variable wanted to be translated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
grid.set_cell_raw((x, y), Some(cell));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_column_after(&mut self, (x, y): (usize, usize)) {
|
pub fn insert_column_after(&mut self, (x, y): (usize, usize)) {
|
||||||
self.insert_column_before((x + 1, y));
|
self.insert_column_before((x + 1, y));
|
||||||
}
|
}
|
||||||
/// Iterate over the entire grid and see where
|
|
||||||
/// the farthest modified cell is.
|
|
||||||
#[must_use]
|
|
||||||
fn max(&self) -> (usize, usize) {
|
|
||||||
let mut max_x = 0;
|
|
||||||
let mut max_y = 0;
|
|
||||||
|
|
||||||
for (xi, x) in self.cells.iter().enumerate() {
|
|
||||||
for (yi, cell) in x.iter().enumerate() {
|
|
||||||
if cell.is_some() {
|
|
||||||
if yi > max_y {
|
|
||||||
max_y = yi
|
|
||||||
}
|
|
||||||
if xi > max_x {
|
|
||||||
max_x = xi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(max_x, max_y)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn max_y_at_x(&self, x: usize) -> usize {
|
|
||||||
let mut max_y = 0;
|
|
||||||
if let Some(col) = self.cells.get(x) {
|
|
||||||
// we could do fancy things like .take_while(not null) but then
|
|
||||||
// we would have to deal with empty cells and stuff, which sounds
|
|
||||||
// boring. This will be fast "enough", considering the grid is
|
|
||||||
// probably only like 1k cells
|
|
||||||
for (yi, cell) in col.iter().enumerate() {
|
|
||||||
if cell.is_some() {
|
|
||||||
if yi > max_y {
|
|
||||||
max_y = yi
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
max_y
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Only evaluates equations, such as `=10` or `=A1/C2`, not
|
/// Only evaluates equations, such as `=10` or `=A1/C2`, not
|
||||||
/// strings or numbers.
|
/// strings or numbers.
|
||||||
pub fn evaluate(&self, mut eq: &str) -> Result<f64, String> {
|
pub fn evaluate(&self, mut eq: &str) -> Result<CellType, String> {
|
||||||
if eq.starts_with('=') {
|
if eq.starts_with('=') {
|
||||||
eq = &eq[1..];
|
eq = &eq[1..];
|
||||||
} else {
|
} else {
|
||||||
@@ -353,11 +434,15 @@ impl Grid {
|
|||||||
if v.is_number() {
|
if v.is_number() {
|
||||||
if v.is_float() {
|
if v.is_float() {
|
||||||
let val = v.as_float().expect("Value lied about being a float");
|
let val = v.as_float().expect("Value lied about being a float");
|
||||||
return Ok(val);
|
return Ok(CellType::Number(val));
|
||||||
} else if v.is_int() {
|
} else if v.is_int() {
|
||||||
let i = v.as_int().expect("Value lied about being an int");
|
let val = v.as_int().expect("Value lied about being an int");
|
||||||
return Ok(i as f64);
|
return Ok(CellType::Number(val as f64));
|
||||||
}
|
}
|
||||||
|
} else if v.is_string() {
|
||||||
|
// ^^ This allows for functions to return a string
|
||||||
|
let s = v.as_string().expect("Value lied about being a String");
|
||||||
|
return Ok(CellType::String(s));
|
||||||
}
|
}
|
||||||
return Err("Result is NaN".to_string());
|
return Err("Result is NaN".to_string());
|
||||||
};
|
};
|
||||||
@@ -370,10 +455,7 @@ impl Grid {
|
|||||||
EvalexprError::VariableIdentifierNotFound(var_not_found) => {
|
EvalexprError::VariableIdentifierNotFound(var_not_found) => {
|
||||||
return Err(format!("\"{var_not_found}\" is not a variable"));
|
return Err(format!("\"{var_not_found}\" is not a variable"));
|
||||||
}
|
}
|
||||||
EvalexprError::TypeError {
|
EvalexprError::TypeError { expected: e, actual: a } => {
|
||||||
expected: e,
|
|
||||||
actual: a,
|
|
||||||
} => {
|
|
||||||
// IE: You put a string into a function that wants a float
|
// IE: You put a string into a function that wants a float
|
||||||
return Err(format!("Wanted {e:?}, got {a}"));
|
return Err(format!("Wanted {e:?}, got {a}"));
|
||||||
}
|
}
|
||||||
@@ -382,12 +464,22 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn char_to_idx(i: &str) -> usize {
|
||||||
|
let x_idx = i
|
||||||
|
.chars()
|
||||||
|
.filter(|f| f.is_alphabetic())
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
|
||||||
|
.fold(0, |a, b| a + b);
|
||||||
|
x_idx
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
/// Parse values in the format of A0, C10 ZZ99, etc, and
|
||||||
/// turn them into an X,Y index.
|
/// turn them into an X,Y index.
|
||||||
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
pub fn parse_to_idx(i: &str) -> Option<(usize, usize)> {
|
||||||
let i = i.replace('$', "");
|
let i = i.replace('$', "");
|
||||||
|
|
||||||
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<Vec<char>>();
|
let chars = i.chars().take_while(|c| c.is_alphabetic()).collect::<String>();
|
||||||
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
let nums = i.chars().skip(chars.len()).take_while(|c| c.is_numeric()).collect::<String>();
|
||||||
|
|
||||||
// At least half the arguments are gone
|
// At least half the arguments are gone
|
||||||
@@ -396,11 +488,7 @@ impl Grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get the x index from the chars
|
// get the x index from the chars
|
||||||
let x_idx = chars
|
let x_idx = Self::char_to_idx(&chars);
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx))
|
|
||||||
.fold(0, |a, b| a + b);
|
|
||||||
|
|
||||||
// get the y index from the numbers
|
// get the y index from the numbers
|
||||||
if let Ok(y_idx) = nums.parse::<usize>() {
|
if let Ok(y_idx) = nums.parse::<usize>() {
|
||||||
@@ -412,15 +500,21 @@ impl Grid {
|
|||||||
|
|
||||||
/// Helper for tests
|
/// Helper for tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
/// Don't ever remove this from being just a test-helper.
|
||||||
|
/// This function doesn't correctly use the undo/redo api, which would require doing
|
||||||
|
/// transactions on the grid instead of direct access.
|
||||||
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
|
pub fn set_cell<T: Into<CellType>>(&mut self, cell_id: &str, val: T) {
|
||||||
if let Some(loc) = Self::parse_to_idx(cell_id) {
|
if let Some(loc) = Self::parse_to_idx(cell_id) {
|
||||||
self.set_cell_raw(loc, Some(val));
|
self.set_cell_raw(loc, Some(val));
|
||||||
}
|
}
|
||||||
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated]
|
||||||
|
/// You should get the grid then transact on the grid it's self
|
||||||
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
|
pub fn set_cell_raw<T: Into<CellType>>(&mut self, (x, y): (usize, usize), val: Option<T>) {
|
||||||
// TODO check oob
|
// TODO check oob
|
||||||
self.cells[x][y] = val.map(|v| v.into());
|
self.get_grid_mut().set_cell_raw((x,y), val.map(|v| v.into()));
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +533,7 @@ impl Grid {
|
|||||||
if x >= LEN || y >= LEN {
|
if x >= LEN || y >= LEN {
|
||||||
return &None;
|
return &None;
|
||||||
}
|
}
|
||||||
&self.cells[x][y]
|
&self.get_grid().get_cell_raw(x,y)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn num_to_char(idx: usize) -> String {
|
pub fn num_to_char(idx: usize) -> String {
|
||||||
@@ -492,11 +586,11 @@ fn saving_csv() {
|
|||||||
// insure that the cells are there
|
// insure that the cells are there
|
||||||
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 11.0);
|
assert_eq!(res, (11.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
||||||
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 121.0);
|
assert_eq!(res, (121.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
||||||
|
|
||||||
// set saving the file
|
// set saving the file
|
||||||
@@ -535,16 +629,16 @@ fn saving_neoscim() {
|
|||||||
// insure that the cells are there
|
// insure that the cells are there
|
||||||
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(0, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 11.0);
|
assert_eq!(res, (11.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
assert_eq!(cell.escaped_csv_string(), "=A9+A$0");
|
||||||
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
let cell = app.grid.get_cell_raw(1, 10).as_ref().expect("Should've been set");
|
||||||
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&cell.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 121.0);
|
assert_eq!(res, (121.0).into());
|
||||||
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
assert_eq!(cell.escaped_csv_string(), "=A10^2");
|
||||||
|
|
||||||
// set saving the file
|
// set saving the file
|
||||||
let filename= "/tmp/file.neoscim";
|
let filename = format!("/tmp/file.{CUSTOM_EXT}");
|
||||||
app.grid.save_to(filename).expect("This will only work on linux systems");
|
app.grid.save_to(&filename).expect("This will only work on linux systems");
|
||||||
let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file");
|
let mut file = fs::OpenOptions::new().read(true).open(filename).expect("Just wrote the file");
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
file.read_to_string(&mut buf).expect("Just opened the file");
|
file.read_to_string(&mut buf).expect("Just opened the file");
|
||||||
@@ -557,7 +651,7 @@ fn saving_neoscim() {
|
|||||||
fn cell_strings() {
|
fn cell_strings() {
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
assert!(&grid.cells[0][0].is_none());
|
assert!(&grid.get_grid().get_cell_raw(0,0).is_none());
|
||||||
grid.set_cell("A0", "Hello".to_string());
|
grid.set_cell("A0", "Hello".to_string());
|
||||||
assert!(grid.get_cell("A0").is_some());
|
assert!(grid.get_cell("A0").is_some());
|
||||||
|
|
||||||
@@ -580,6 +674,7 @@ fn alphanumeric_indexing() {
|
|||||||
assert_eq!(Grid::parse_to_idx("A"), None);
|
assert_eq!(Grid::parse_to_idx("A"), None);
|
||||||
assert_eq!(Grid::parse_to_idx(":"), None);
|
assert_eq!(Grid::parse_to_idx(":"), None);
|
||||||
assert_eq!(Grid::parse_to_idx("="), None);
|
assert_eq!(Grid::parse_to_idx("="), None);
|
||||||
|
assert_eq!(Grid::parse_to_idx("A:A"), None);
|
||||||
|
|
||||||
assert_eq!(Grid::num_to_char(0).trim(), "A");
|
assert_eq!(Grid::num_to_char(0).trim(), "A");
|
||||||
assert_eq!(Grid::num_to_char(25).trim(), "Z");
|
assert_eq!(Grid::num_to_char(25).trim(), "Z");
|
||||||
@@ -600,31 +695,31 @@ fn valid_equations() {
|
|||||||
// cell math
|
// cell math
|
||||||
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 3.);
|
assert_eq!(res, (3.).into());
|
||||||
|
|
||||||
// divide floats
|
// divide floats
|
||||||
grid.set_cell("D0", "=5./2.".to_string());
|
grid.set_cell("D0", "=5./2.".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// Float / Int mix
|
// Float / Int mix
|
||||||
grid.set_cell("D0", "=5./2".to_string());
|
grid.set_cell("D0", "=5./2".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// divide "ints" (should become floats)
|
// divide "ints" (should become floats)
|
||||||
grid.set_cell("D0", "=5/2".to_string());
|
grid.set_cell("D0", "=5/2".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 2.5);
|
assert_eq!(res, (2.5).into());
|
||||||
|
|
||||||
// Non-equation that should still be valid
|
// Non-equation that should still be valid
|
||||||
grid.set_cell("D0", "=10".to_string());
|
grid.set_cell("D0", "=10".to_string());
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
let cell = grid.get_cell("D0").as_ref().expect("I just set this");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
let res = grid.evaluate(&cell.to_string()).expect("Should be ok");
|
||||||
assert_eq!(res, 10.);
|
assert_eq!(res, (10.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cell = output of Cell = value of Cells.
|
// Cell = output of Cell = value of Cells.
|
||||||
@@ -641,7 +736,7 @@ fn fn_of_fn() {
|
|||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 6.);
|
assert_eq!(res.unwrap(), (6.).into());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
panic!("Cell not found");
|
panic!("Cell not found");
|
||||||
@@ -677,7 +772,7 @@ fn invalid_equations() {
|
|||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert!(res.is_ok_and(|v| v == 10.));
|
assert!(res.is_ok_and(|v| v == (10.).into()));
|
||||||
|
|
||||||
// Trailing comma in function call
|
// Trailing comma in function call
|
||||||
grid.set_cell("A0", 5.);
|
grid.set_cell("A0", 5.);
|
||||||
@@ -685,7 +780,7 @@ fn invalid_equations() {
|
|||||||
grid.set_cell("B0", "=avg(A0,A1,)".to_string());
|
grid.set_cell("B0", "=avg(A0,A1,)".to_string());
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert_eq!(res.unwrap(), 7.5);
|
assert_eq!(res.unwrap(), (7.5).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -693,17 +788,17 @@ fn grid_max() {
|
|||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
grid.set_cell("A0", 1.);
|
grid.set_cell("A0", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 0);
|
assert_eq!(mx, 0);
|
||||||
assert_eq!(my, 0);
|
assert_eq!(my, 0);
|
||||||
|
|
||||||
grid.set_cell("B0", 1.);
|
grid.set_cell("B0", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 1);
|
assert_eq!(mx, 1);
|
||||||
assert_eq!(my, 0);
|
assert_eq!(my, 0);
|
||||||
|
|
||||||
grid.set_cell("B5", 1.);
|
grid.set_cell("B5", 1.);
|
||||||
let (mx, my) = grid.max();
|
let (mx, my) = grid.get_grid().max();
|
||||||
assert_eq!(mx, 1);
|
assert_eq!(mx, 1);
|
||||||
assert_eq!(my, 5);
|
assert_eq!(my, 5);
|
||||||
}
|
}
|
||||||
@@ -716,19 +811,19 @@ fn avg_function() {
|
|||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 5.);
|
assert_eq!(res.unwrap(), (5.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(5,10)".to_string());
|
grid.set_cell("A0", "=avg(5,10)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 7.5);
|
assert_eq!(res.unwrap(), (7.5).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(5,10,15)".to_string());
|
grid.set_cell("A0", "=avg(5,10,15)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 10.);
|
assert_eq!(res.unwrap(), (10.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=avg(foo)".to_string());
|
grid.set_cell("A0", "=avg(foo)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
@@ -754,13 +849,13 @@ fn sum_function() {
|
|||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 5.);
|
assert_eq!(res.unwrap(), (5.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=sum(5,10)".to_string());
|
grid.set_cell("A0", "=sum(5,10)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 15.);
|
assert_eq!(res.unwrap(), (15.).into());
|
||||||
|
|
||||||
grid.set_cell("A0", "=sum(foo)".to_string());
|
grid.set_cell("A0", "=sum(foo)".to_string());
|
||||||
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
@@ -786,11 +881,11 @@ fn xlookup_function() {
|
|||||||
grid.set_cell("A1", "Sarah".to_string());
|
grid.set_cell("A1", "Sarah".to_string());
|
||||||
grid.set_cell("C0", 31.);
|
grid.set_cell("C0", 31.);
|
||||||
grid.set_cell("C1", 41.);
|
grid.set_cell("C1", 41.);
|
||||||
grid.set_cell("B0", "=xlookup(A:A,\"Bobby\",C:C)".to_string());
|
grid.set_cell("B0", "=xlookup(\"Bobby\",A:A,C:C)".to_string());
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set the cell");
|
||||||
let res = grid.evaluate(&cell.to_string());
|
let res = grid.evaluate(&cell.to_string());
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
assert_eq!(res.unwrap(), 31.);
|
assert_eq!(res.unwrap(), (31.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -844,6 +939,19 @@ fn parse_csv() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_ranges() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
|
||||||
|
grid.set_cell("A0", 2.);
|
||||||
|
grid.set_cell("A1", 1.);
|
||||||
|
// ASCII to number conversion needs to not overflow
|
||||||
|
grid.set_cell("B0", "=sum($:A)".to_string());
|
||||||
|
|
||||||
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
|
let _ = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ranges() {
|
fn ranges() {
|
||||||
let mut grid = Grid::new();
|
let mut grid = Grid::new();
|
||||||
@@ -855,13 +963,13 @@ fn ranges() {
|
|||||||
// range with numbers
|
// range with numbers
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 3.);
|
assert_eq!(res, (3.).into());
|
||||||
|
|
||||||
// use range output as input for other function
|
// use range output as input for other function
|
||||||
grid.set_cell("B1", "=B0*2".to_string());
|
grid.set_cell("B1", "=B0*2".to_string());
|
||||||
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B1").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
|
|
||||||
// use equation outputs as range input
|
// use equation outputs as range input
|
||||||
grid.set_cell("A2", "=C0+1".to_string());
|
grid.set_cell("A2", "=C0+1".to_string());
|
||||||
@@ -869,11 +977,11 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("A2").as_ref().expect("Just set it");
|
let cell = grid.get_cell("A2").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
|
|
||||||
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("B0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 9.);
|
assert_eq!(res, (9.).into());
|
||||||
|
|
||||||
// use function outputs as range input
|
// use function outputs as range input
|
||||||
grid.set_cell("B1", 2.);
|
grid.set_cell("B1", 2.);
|
||||||
@@ -882,7 +990,7 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("C0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 5.);
|
assert_eq!(res, (5.).into());
|
||||||
|
|
||||||
// use range outputs as range input
|
// use range outputs as range input
|
||||||
grid.set_cell("D0", "=sum(C:C)".to_string());
|
grid.set_cell("D0", "=sum(C:C)".to_string());
|
||||||
@@ -890,7 +998,7 @@ fn ranges() {
|
|||||||
|
|
||||||
let cell = grid.get_cell("D0").as_ref().expect("Just set it");
|
let cell = grid.get_cell("D0").as_ref().expect("Just set it");
|
||||||
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
let res = grid.evaluate(&cell.to_string()).expect("Should evaluate.");
|
||||||
assert_eq!(res, 6.);
|
assert_eq!(res, (6.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1041,7 +1149,7 @@ fn insert_row_above_3() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn cell_eval_depth() {
|
fn cell_eval_depth() {
|
||||||
use crate::app::mode::*;
|
use crate::app::mode::*;
|
||||||
let mut app= App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
app.grid.set_cell("A0", 1.);
|
app.grid.set_cell("A0", 1.);
|
||||||
app.grid.set_cell("A1", "=A0+$A$0".to_string());
|
app.grid.set_cell("A1", "=A0+$A$0".to_string());
|
||||||
@@ -1060,6 +1168,34 @@ fn cell_eval_depth() {
|
|||||||
assert_eq!(c.to_string(), "=A5+$A$0");
|
assert_eq!(c.to_string(), "=A5+$A$0");
|
||||||
|
|
||||||
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
|
let res = app.grid.evaluate(&c.to_string()).expect("Should evaluate");
|
||||||
assert_eq!(res, 7.);
|
assert_eq!(res, (7.).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_string_from_fn() {
|
||||||
|
let mut grid = Grid::new();
|
||||||
|
grid.set_cell("A0", "=if(2>1, \"A\", \"B\")".to_string()); // true, A
|
||||||
|
grid.set_cell("A1", "=if(1>2, \"A\", \"B\")".to_string()); // false, B
|
||||||
|
|
||||||
|
let cell = grid.get_cell("A0").as_ref().expect("Just set the cell");
|
||||||
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
assert!(res.is_ok());
|
||||||
|
if let Ok(f) = res {
|
||||||
|
if let CellType::String(s) = f {
|
||||||
|
assert_eq!("A", s);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = grid.get_cell("A1").as_ref().expect("Just set the cell");
|
||||||
|
let res = grid.evaluate(&cell.to_string());
|
||||||
|
assert!(res.is_ok());
|
||||||
|
if let Ok(f) = res {
|
||||||
|
if let CellType::String(s) = f {
|
||||||
|
assert_eq!("B", s);
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,42 +54,46 @@ impl CellType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `replace_fn` takes the string, the old value, and then the new value.
|
||||||
|
/// It can be thought of as `echo $1 | sed s/$2/$3/g`
|
||||||
pub fn custom_translate_cell(&self, from: (usize, usize), to: (usize, usize), replace_fn: impl Fn(&str, &str, &str) -> String) -> CellType {
|
pub fn custom_translate_cell(&self, from: (usize, usize), to: (usize, usize), replace_fn: impl Fn(&str, &str, &str) -> String) -> CellType {
|
||||||
match self {
|
match self {
|
||||||
// don't translate non-equations
|
// don't translate non-equations
|
||||||
CellType::Number(_) | CellType::String(_) => return self.clone(),
|
CellType::Number(_) | CellType::String(_) => return self.clone(),
|
||||||
CellType::Equation(eq) => {
|
CellType::Equation(eq) => {
|
||||||
// extract all the variables
|
// Populate the context
|
||||||
let ctx = ExtractionContext::new();
|
let ctx = ExtractionContext::new();
|
||||||
let _ = eval_with_context(eq, &ctx);
|
let _ = eval_with_context(&eq[1..], &ctx);
|
||||||
|
|
||||||
let mut rolling = eq.clone();
|
let mut equation = eq.clone();
|
||||||
// translate standard vars A0 -> A1
|
// translate standard vars A0 -> A1
|
||||||
|
// extract all the variables
|
||||||
for old_var in ctx.dump_vars() {
|
for old_var in ctx.dump_vars() {
|
||||||
let mut lock_x = false;
|
let mut lock_x = false;
|
||||||
let mut lock_y = false;
|
let mut lock_y = false;
|
||||||
|
|
||||||
if old_var.contains('$') {
|
if old_var.contains('$') {
|
||||||
let locations = old_var.char_indices().filter(|(_, c)| *c == '$').map(|(i, _)| i).collect::<Vec<usize>>();
|
let locations = old_var
|
||||||
|
.char_indices()
|
||||||
|
.filter(|(_, c)| *c == '$').map(|(i, _)| i)
|
||||||
|
.collect::<Vec<usize>>();
|
||||||
match locations.len() {
|
match locations.len() {
|
||||||
1 => {
|
1 => {
|
||||||
if locations[0] == 0 {
|
if locations[0] == 0 {
|
||||||
// locking the X axis (A,B,C...)
|
// locking the X axis (A,B,C...)
|
||||||
lock_x = true;
|
lock_x = true;
|
||||||
} else if locations[0] < old_var.len() {
|
} else {
|
||||||
// inside the string somewhere, gonna assume this means to lock Y (1,2,3...)
|
// inside the string somewhere, gonna assume this means to lock Y (1,2,3...)
|
||||||
lock_y = true;
|
lock_y = true;
|
||||||
|
|
||||||
} else {
|
|
||||||
// where tf is this dollar sign?
|
|
||||||
// (It's somewhere malformed, like A0$ or something)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
// YOLO, lock both X & Y
|
// Ignore this variable all together, effectively lockng X & Y
|
||||||
continue; // just pretend you never even saw this var
|
continue;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
// There are 0 or >2 "$" in this string.
|
||||||
|
//
|
||||||
// Could probably optimize the code or something so you only go over the string
|
// Could probably optimize the code or something so you only go over the string
|
||||||
// once, instead of contains() then getting the indexes of where it is.
|
// once, instead of contains() then getting the indexes of where it is.
|
||||||
// You could then put your no-$ code here.
|
// You could then put your no-$ code here.
|
||||||
@@ -98,6 +102,7 @@ impl CellType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
|
if let Some((src_x, src_y)) = Grid::parse_to_idx(&old_var) {
|
||||||
|
// Use i32s instead of usize in case of negative numbers
|
||||||
let (x1, y1) = from;
|
let (x1, y1) = from;
|
||||||
let x1 = x1 as i32;
|
let x1 = x1 as i32;
|
||||||
let y1 = y1 as i32;
|
let y1 = y1 as i32;
|
||||||
@@ -120,6 +125,7 @@ impl CellType {
|
|||||||
let alpha = Grid::num_to_char(dest_x);
|
let alpha = Grid::num_to_char(dest_x);
|
||||||
let alpha = alpha.trim();
|
let alpha = alpha.trim();
|
||||||
|
|
||||||
|
// Persist the "$" locking
|
||||||
let new_var = if lock_x {
|
let new_var = if lock_x {
|
||||||
format!("${alpha}{dest_y}")
|
format!("${alpha}{dest_y}")
|
||||||
} else if lock_y {
|
} else if lock_y {
|
||||||
@@ -128,15 +134,48 @@ impl CellType {
|
|||||||
format!("{alpha}{dest_y}")
|
format!("{alpha}{dest_y}")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// swap out vars
|
// swap out vars
|
||||||
rolling = replace_fn(&rolling, &old_var, &new_var);
|
equation = replace_fn(&equation, &old_var, &new_var);
|
||||||
// rolling = rolling.replace(&old_var, &new_var);
|
// rolling = rolling.replace(&old_var, &new_var);
|
||||||
} else {
|
} else {
|
||||||
// why you coping invalid stuff, nerd?
|
// why you coping invalid stuff, nerd?
|
||||||
|
//
|
||||||
|
// could be copying a range
|
||||||
|
if old_var.contains(':') {
|
||||||
|
let parts = old_var.split(':').collect::<Vec<&str>>();
|
||||||
|
// This means the var was formatted as X:X
|
||||||
|
if parts.len() == 2 {
|
||||||
|
// how far is the movement?
|
||||||
|
let dx = to.0 as i32 - from.0 as i32;
|
||||||
|
|
||||||
|
let range_start = parts[0];
|
||||||
|
let range_end = parts[1];
|
||||||
|
|
||||||
|
// get the letters as numbers
|
||||||
|
let xs = Grid::char_to_idx(range_start) as i32;
|
||||||
|
let xe = Grid::char_to_idx(range_end) as i32;
|
||||||
|
|
||||||
|
// apply movement
|
||||||
|
let mut new_range_start = xs+dx;
|
||||||
|
let mut new_range_end = xe+dx;
|
||||||
|
|
||||||
|
// bottom out at 0
|
||||||
|
if new_range_start < 0 {
|
||||||
|
new_range_start = 0;
|
||||||
|
}
|
||||||
|
if new_range_end < 0 {
|
||||||
|
new_range_end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert the index back into a letter and then submit it
|
||||||
|
let start = Grid::num_to_char(new_range_start as usize);
|
||||||
|
let end = Grid::num_to_char(new_range_end as usize);
|
||||||
|
equation = replace_fn(&equation, &old_var, &format!("{}:{}", start.trim(), end.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rolling.into();
|
return equation.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,3 +195,15 @@ impl Display for CellType {
|
|||||||
write!(f, "{d}")
|
write!(f, "{d}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CellType {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Number(left), Self::Number(right)) => left == right,
|
||||||
|
(Self::String(left), Self::String(right)) => left == right,
|
||||||
|
(Self::Equation(left), Self::Equation(right)) => left == right,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ impl<'a> CallbackContext<'a> {
|
|||||||
let as_index = |s: &str| {
|
let as_index = |s: &str| {
|
||||||
s.char_indices()
|
s.char_indices()
|
||||||
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
|
// .filter(|f| f.1 as u8 >= 97) // prevent sub with overflow errors
|
||||||
.map(|(idx, c)| (c.to_ascii_lowercase() as usize - 97) + (26 * idx))
|
.map(|(idx, c)| ((c.to_ascii_lowercase() as usize).saturating_sub(97)) + (26 * idx))
|
||||||
.fold(0, |a, b| a + b)
|
.fold(0, |a, b| a + b)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,8 +34,8 @@ impl<'a> CallbackContext<'a> {
|
|||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
for x in start_idx..=end_idx {
|
for x in start_idx..=end_idx {
|
||||||
for y in 0..=self.variables.max_y_at_x(x) {
|
for y in 0..=self.variables.get_grid().max_y_at_x(x) {
|
||||||
if let Some(s) = self.variables.get_cell_raw(x, y) {
|
if let Some(s) = self.variables.get_grid().get_cell_raw(x, y) {
|
||||||
buf.push(s);
|
buf.push(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,13 +104,13 @@ impl<'a> CallbackContext<'a> {
|
|||||||
functions.insert(
|
functions.insert(
|
||||||
"xlookup".to_string(),
|
"xlookup".to_string(),
|
||||||
Function::new(|arg| {
|
Function::new(|arg| {
|
||||||
let expected = vec![ValueType::Tuple, ValueType::String, ValueType::Tuple];
|
let expected = vec![ValueType::String, ValueType::Tuple, ValueType::Tuple];
|
||||||
if arg.is_tuple() {
|
if arg.is_tuple() {
|
||||||
let args = arg.as_tuple()?;
|
let args = arg.as_tuple()?;
|
||||||
|
|
||||||
if args.len() == 3 {
|
if args.len() == 3 {
|
||||||
let lookup_array = &args[0];
|
let lookup_value = &args[0];
|
||||||
let lookup_value = &args[1];
|
let lookup_array = &args[1];
|
||||||
let return_array = &args[2];
|
let return_array = &args[2];
|
||||||
|
|
||||||
if lookup_array.is_tuple() && return_array.is_tuple() {
|
if lookup_array.is_tuple() && return_array.is_tuple() {
|
||||||
@@ -162,8 +162,8 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
|
if let Ok(mut trail) = self.eval_breadcrumbs.write() {
|
||||||
let find = trail.iter().filter(|id| *id == identifier).collect::<Vec<&String>>();
|
let find = trail.iter().filter(|id| *id == identifier).count();
|
||||||
if find.len() > 0 {
|
if find > 0 {
|
||||||
// recursion detected
|
// recursion detected
|
||||||
return None;
|
return None;
|
||||||
} else {
|
} else {
|
||||||
@@ -195,7 +195,7 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
match e {
|
match e {
|
||||||
EvalexprError::VariableIdentifierNotFound(_) => {
|
EvalexprError::VariableIdentifierNotFound(_err) => {
|
||||||
// If the variable isn't found, that's ~~probably~~ because
|
// If the variable isn't found, that's ~~probably~~ because
|
||||||
// of recursive reference, considering all references
|
// of recursive reference, considering all references
|
||||||
// are grabbed straight from the table.
|
// are grabbed straight from the table.
|
||||||
@@ -217,8 +217,14 @@ impl<'a> Context for CallbackContext<'a> {
|
|||||||
CellType::Number(e) => vals.push(Value::Float(*e)),
|
CellType::Number(e) => vals.push(Value::Float(*e)),
|
||||||
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
CellType::String(s) => vals.push(Value::String(s.to_owned())),
|
||||||
CellType::Equation(eq) => {
|
CellType::Equation(eq) => {
|
||||||
if let Ok(val) = eval_with_context(&eq[1..], self) {
|
match eval_with_context(&eq[1..], self) {
|
||||||
vals.push(val);
|
Ok(val) => vals.push(val),
|
||||||
|
Err(_err) => {
|
||||||
|
// At this point we are getting an error because
|
||||||
|
// recursion protection made this equation return
|
||||||
|
// None. We now don't get any evaluation.
|
||||||
|
return None
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,10 +276,19 @@ impl ExtractionContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn dump_vars(&self) -> Vec<String> {
|
pub fn dump_vars(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.var_registry.read() { r.clone() } else { Vec::new() }
|
if let Ok(r) = self.var_registry.read() {
|
||||||
|
r.clone()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn dump_fns(&self) -> Vec<String> {
|
pub fn dump_fns(&self) -> Vec<String> {
|
||||||
if let Ok(r) = self.fn_registry.read() { r.clone() } else { Vec::new() }
|
if let Ok(r) = self.fn_registry.read() {
|
||||||
|
r.clone()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +316,7 @@ impl Context for ExtractionContext {
|
|||||||
} else {
|
} else {
|
||||||
panic!("The RwLock should always be write-able")
|
panic!("The RwLock should always be write-able")
|
||||||
}
|
}
|
||||||
// Ok(Value::Int(1))
|
Ok(Value::Int(1))
|
||||||
unimplemented!("Extracting function identifier not implemented yet")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn are_builtin_functions_disabled(&self) -> bool {
|
fn are_builtin_functions_disabled(&self) -> bool {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use crate::app::{
|
|||||||
app::App,
|
app::App,
|
||||||
error_msg::StatusMessage,
|
error_msg::StatusMessage,
|
||||||
logic::{
|
logic::{
|
||||||
calc::{CSV_EXT, CUSTOM_EXT, Grid, LEN},
|
calc::{Grid, LEN},
|
||||||
cell::CellType,
|
cell::CellType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -70,25 +70,11 @@ impl Mode {
|
|||||||
"w" => {
|
"w" => {
|
||||||
// first try the passed argument as file
|
// first try the passed argument as file
|
||||||
if let Some(arg) = args.get(1) {
|
if let Some(arg) = args.get(1) {
|
||||||
let mut path: PathBuf = arg.into();
|
let path: PathBuf = arg.into();
|
||||||
match path.extension() {
|
|
||||||
Some(s) => {
|
|
||||||
match s.to_str() {
|
|
||||||
// leave the file alone, it already has
|
|
||||||
// a valid extension
|
|
||||||
Some(CSV_EXT) | Some(CUSTOM_EXT) => {}
|
|
||||||
_ => {
|
|
||||||
path.add_extension(CUSTOM_EXT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
path.add_extension(CUSTOM_EXT);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO Check if the file exists, but the program wasn't opened with it. We might be accidentally overwriting something else.
|
// TODO Check if the file we are writing to exists, since
|
||||||
// let mut file = fs::OpenOptions::new().write(true).append(false).truncate(true).create(true).open(path)?;
|
// this code path already knows that we are writing to a new file.
|
||||||
|
// We might be accidentally overwriting something else.
|
||||||
|
|
||||||
if let Err(e) = app.grid.save_to(&path) {
|
if let Err(e) = app.grid.save_to(&path) {
|
||||||
app.msg = StatusMessage::error(format!("{e}"));
|
app.msg = StatusMessage::error(format!("{e}"));
|
||||||
@@ -189,6 +175,21 @@ impl Mode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match args[0] {
|
match args[0] {
|
||||||
|
"f" | "fill" => {
|
||||||
|
for (i, x) in (low_x..=hi_x).enumerate() {
|
||||||
|
for (j, y) in (low_y..=hi_y).enumerate() {
|
||||||
|
let arg = args.get(1)
|
||||||
|
.map(|s| s.replace("xi", &i.to_string()))
|
||||||
|
.map(|s| s.replace("yi", &j.to_string()))
|
||||||
|
.map(|s| s.replace("x", &x.to_string()))
|
||||||
|
.map(|s| s.replace("y", &y.to_string()))
|
||||||
|
;
|
||||||
|
app.grid.set_cell_raw((x,y), arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mode = Mode::Normal
|
||||||
|
}
|
||||||
"export" => {
|
"export" => {
|
||||||
if let Some(arg1) = args.get(1) {
|
if let Some(arg1) = args.get(1) {
|
||||||
save_range(&arg1);
|
save_range(&arg1);
|
||||||
@@ -216,10 +217,16 @@ impl Mode {
|
|||||||
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
let s = s.replace("$OUTPUT", "/tmp/output.png");
|
||||||
let _ = fs::write("/tmp/plot.p", s);
|
let _ = fs::write("/tmp/plot.p", s);
|
||||||
|
|
||||||
let _ = Command::new("gnuplot").arg("/tmp/plot.p").output();
|
let cmd_res= Command::new("gnuplot").arg("/tmp/plot.p").output();
|
||||||
let _ = fs::copy("/tmp/output.png", output_filename);
|
if let Err(err) = cmd_res {
|
||||||
|
match err.kind() {
|
||||||
app.msg = StatusMessage::info("Wrote gnuplot data to /tmp");
|
std::io::ErrorKind::NotFound => app.msg = StatusMessage::error("Error - Is gnuplot installed?"),
|
||||||
|
_ => app.msg = StatusMessage::error(format!("{err}")),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let _ = fs::copy("/tmp/output.png", output_filename);
|
||||||
|
app.msg = StatusMessage::info(format!("Created {output_filename}. Artifacts are in /tmp"));
|
||||||
|
}
|
||||||
app.mode = Mode::Normal
|
app.mode = Mode::Normal
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -260,6 +267,18 @@ impl Mode {
|
|||||||
app.grid.mv_cursor_to(0, y);
|
app.grid.mv_cursor_to(0, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Go to end of row
|
||||||
|
'$' => {
|
||||||
|
let (_, y) = app.grid.cursor();
|
||||||
|
app.grid.mv_cursor_to(super::logic::calc::LEN, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Go to bottom of column
|
||||||
|
'G' => {
|
||||||
|
let (x, _) = app.grid.cursor();
|
||||||
|
app.grid.mv_cursor_to(x, super::logic::calc::LEN,);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// edit cell
|
// edit cell
|
||||||
'i' | 'a' => {
|
'i' | 'a' => {
|
||||||
let (x, y) = app.grid.cursor();
|
let (x, y) = app.grid.cursor();
|
||||||
@@ -373,6 +392,34 @@ impl Mode {
|
|||||||
app.grid.mv_cursor_to(x, 0);
|
app.grid.mv_cursor_to(x, 0);
|
||||||
app.mode = Mode::Normal;
|
app.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
|
// Go to the bottom of the current window
|
||||||
|
("g", 'G') => {
|
||||||
|
let (x, _) = app.grid.cursor();
|
||||||
|
let (_, y_height) = app.screen.get_screen_size(&app.vars);
|
||||||
|
let y_origin = app.screen.scroll_y();
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(x, y_origin+y_height);
|
||||||
|
app.mode = Mode::Normal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Go to the right edge of the current window
|
||||||
|
("g", '$') => {
|
||||||
|
let (_, y) = app.grid.cursor();
|
||||||
|
let (x_width, _) = app.screen.get_screen_size(&app.vars);
|
||||||
|
let x_origin = app.screen.scroll_x();
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(x_origin+x_width, y);
|
||||||
|
app.mode = Mode::Normal;
|
||||||
|
}
|
||||||
|
// Go to the left edge of the current window
|
||||||
|
("g", '0') => {
|
||||||
|
let (_, y) = app.grid.cursor();
|
||||||
|
let x_origin = app.screen.scroll_x();
|
||||||
|
|
||||||
|
app.grid.mv_cursor_to(x_origin, y);
|
||||||
|
app.mode = Mode::Normal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
// center screen to cursor
|
// center screen to cursor
|
||||||
("z", 'z') => {
|
("z", 'z') => {
|
||||||
app.screen.center_x(app.grid.cursor(), &app.vars);
|
app.screen.center_x(app.grid.cursor(), &app.vars);
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ use ratatui::prelude;
|
|||||||
use crate::app::logic::calc::LEN;
|
use crate::app::logic::calc::LEN;
|
||||||
|
|
||||||
pub struct ScreenSpace {
|
pub struct ScreenSpace {
|
||||||
/// This is measured in cells
|
/// This is measured in cells.
|
||||||
|
/// This is the top-left cell index.
|
||||||
scroll: (usize, usize),
|
scroll: (usize, usize),
|
||||||
|
/// In chars
|
||||||
default_cell_len: usize,
|
default_cell_len: usize,
|
||||||
|
/// In chars
|
||||||
default_cell_hight: usize,
|
default_cell_hight: usize,
|
||||||
/// This is measured in chars
|
/// This is measured in chars
|
||||||
last_seen_screen_size: RwLock<(usize, usize)>
|
last_seen_screen_size: RwLock<(usize, usize)>
|
||||||
@@ -17,74 +20,84 @@ impl ScreenSpace {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
scroll: (0, 0),
|
scroll: (0, 0),
|
||||||
default_cell_len: 10,
|
default_cell_len: 9,
|
||||||
default_cell_hight: 1,
|
default_cell_hight: 1,
|
||||||
last_seen_screen_size: RwLock::new((0,0))
|
last_seen_screen_size: RwLock::new((0,0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_x(&mut self, (cursor_x, _): (usize, usize), vars: &HashMap<String, String>) {
|
pub fn center_x(&mut self, (cursor_x, _): (usize, usize), vars: &HashMap<String, String>) {
|
||||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
let (x_cells, _) = self.get_screen_size(vars);
|
||||||
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
let x_center = self.scroll_x() + (x_cells/2);
|
||||||
let x_center = self.scroll_x() + (x_cells/2);
|
|
||||||
|
|
||||||
let delta = cursor_x as isize - x_center as isize;
|
let delta = cursor_x as isize - x_center as isize;
|
||||||
self.scroll.0 = self.scroll.0.saturating_add_signed(delta);
|
self.scroll.0 = self.scroll.0.saturating_add_signed(delta);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn center_y(&mut self, (_, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
pub fn center_y(&mut self, (_, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
||||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
let (_, y_cells) = self.get_screen_size(vars);
|
||||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
let y_center = self.scroll_y() + (y_cells/2);
|
||||||
let y_center = self.scroll_y() + (y_cells/2);
|
|
||||||
|
|
||||||
let delta = cursor_y as isize - y_center as isize;
|
let delta = cursor_y as isize - y_center as isize;
|
||||||
self.scroll.1 = self.scroll.1.saturating_add_signed(delta);
|
self.scroll.1 = self.scroll.1.saturating_add_signed(delta);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
/// In chars
|
||||||
|
pub fn get_screen_size(&self, vars: &HashMap<String, String>) -> (usize, usize) {
|
||||||
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
if let Ok(screen_size) = self.last_seen_screen_size.read() {
|
||||||
// ======= X =======
|
// ======= X =======
|
||||||
// screen seems to be 2 cells smaller than it should be
|
// screen seems to be 2 cells smaller than it should be
|
||||||
// this is probably related to issue #6
|
// this is probably related to issue #6
|
||||||
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
let x_cells = (screen_size.0 / self.get_cell_width(vars) as usize) -2;
|
||||||
let lower_x = self.scroll_x();
|
|
||||||
let upper_x = self.scroll_x() + x_cells;
|
|
||||||
|
|
||||||
if cursor_x < lower_x {
|
|
||||||
let delta = lower_x - cursor_x;
|
|
||||||
self.scroll.0 = self.scroll.0.saturating_sub(delta);
|
|
||||||
}
|
|
||||||
if cursor_x > upper_x {
|
|
||||||
let delta = cursor_x - upper_x;
|
|
||||||
self.scroll.0 = self.scroll.0.saturating_add(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======= Y =======
|
// ======= Y =======
|
||||||
// screen seems to be 2 cells smaller than it should be
|
// screen seems to be 2 cells smaller than it should be
|
||||||
// this is probably related to issue #6
|
// this is probably related to issue #6
|
||||||
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
let y_cells = (screen_size.1 / self.get_cell_height(vars) as usize) -2;
|
||||||
let lower_y = self.scroll_y();
|
(x_cells,y_cells)
|
||||||
let upper_y = self.scroll_y() + y_cells;
|
} else {
|
||||||
|
(0,0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cursor_y < lower_y {
|
pub fn scroll_based_on_cursor_location(&mut self, (cursor_x, cursor_y): (usize, usize), vars: &HashMap<String, String>) {
|
||||||
let delta = lower_y - cursor_y;
|
let (x_cells, y_cells) = self.get_screen_size(vars);
|
||||||
self.scroll.1 = self.scroll.1.saturating_sub(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cursor_y > upper_y {
|
|
||||||
let delta = cursor_y - upper_y;
|
let lower_x = self.scroll_x();
|
||||||
self.scroll.1 = self.scroll.1.saturating_add(delta);
|
let upper_x = self.scroll_x() + x_cells;
|
||||||
}
|
|
||||||
|
if cursor_x < lower_x {
|
||||||
|
let delta = lower_x - cursor_x;
|
||||||
|
self.scroll.0 = self.scroll.0.saturating_sub(delta);
|
||||||
|
}
|
||||||
|
if cursor_x > upper_x {
|
||||||
|
let delta = cursor_x - upper_x;
|
||||||
|
self.scroll.0 = self.scroll.0.saturating_add(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let lower_y = self.scroll_y();
|
||||||
|
let upper_y = self.scroll_y() + y_cells;
|
||||||
|
|
||||||
|
if cursor_y < lower_y {
|
||||||
|
let delta = lower_y - cursor_y;
|
||||||
|
self.scroll.1 = self.scroll.1.saturating_sub(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursor_y > upper_y {
|
||||||
|
let delta = cursor_y - upper_y;
|
||||||
|
self.scroll.1 = self.scroll.1.saturating_add(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_x(&self) -> usize {
|
pub fn scroll_x(&self) -> usize {
|
||||||
self.scroll.0
|
self.scroll.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_y(&self) -> usize{
|
pub fn scroll_y(&self) -> usize{
|
||||||
self.scroll.1
|
self.scroll.1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cell_height(&self, vars: &HashMap<String, String>) -> usize {
|
pub fn get_cell_height(&self, vars: &HashMap<String, String>) -> usize {
|
||||||
if let Some(h) = vars.get("height") {
|
if let Some(h) = vars.get("height") {
|
||||||
if let Ok(p) = h.parse::<usize>() {
|
if let Ok(p) = h.parse::<usize>() {
|
||||||
@@ -93,6 +106,7 @@ impl ScreenSpace {
|
|||||||
}
|
}
|
||||||
return self.default_cell_hight
|
return self.default_cell_hight
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cell_width(&self, vars: &HashMap<String, String>) -> usize {
|
pub fn get_cell_width(&self, vars: &HashMap<String, String>) -> usize {
|
||||||
if let Some(h) = vars.get("length") {
|
if let Some(h) = vars.get("length") {
|
||||||
if let Ok(p) = h.parse::<usize>() {
|
if let Ok(p) = h.parse::<usize>() {
|
||||||
@@ -100,19 +114,34 @@ impl ScreenSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.default_cell_len
|
self.default_cell_len
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn how_many_cells_fit_in(&self, area: &prelude::Rect, vars: &HashMap<String, String>) -> (u16, u16) {
|
pub fn how_many_cells_fit_in(&self, area: &prelude::Rect, vars: &HashMap<String, String>) -> (u16, u16) {
|
||||||
if let Ok(mut l) = self.last_seen_screen_size.write() {
|
if let Ok(mut l) = self.last_seen_screen_size.write() {
|
||||||
l.0 = area.width as usize;
|
l.0 = area.width as usize;
|
||||||
l.1 = area.height as usize;
|
l.1 = area.height as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let width = (area.width as usize + calc::get_header_size() -1) / self.get_cell_width(vars);
|
||||||
|
let width = area.width as usize / self.get_cell_width(vars);
|
||||||
|
let height = area.height as usize / self.get_cell_height(vars);
|
||||||
|
|
||||||
let x_max =
|
let x_max =
|
||||||
if area.width as usize / self.get_cell_width(vars) > LEN { LEN - 1 } else { area.width as usize / self.get_cell_width(vars)};
|
if width > LEN {
|
||||||
|
LEN - 1
|
||||||
|
} else {
|
||||||
|
width
|
||||||
|
};
|
||||||
let y_max =
|
let y_max =
|
||||||
if area.height as usize / self.get_cell_height(vars) > LEN { LEN - 1 } else { area.height as usize / self.get_cell_height(vars)};
|
if height > LEN {
|
||||||
|
LEN - 1
|
||||||
|
} else {
|
||||||
|
height
|
||||||
|
};
|
||||||
(x_max as u16, y_max as u16)
|
(x_max as u16, y_max as u16)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ set grid
|
|||||||
set term png size 1280, 720
|
set term png size 1280, 720
|
||||||
set output '$OUTPUT'
|
set output '$OUTPUT'
|
||||||
plot datafile using 1:2 with linespoints linestyle 1
|
plot datafile using 1:2 with linespoints linestyle 1
|
||||||
|
# plot datafile using 1:2 with linespoints linestyle 1, datafile using 1:3, etc...
|
||||||
replot
|
replot
|
||||||
|
|||||||
Reference in New Issue
Block a user