Compare commits
13 commits
801e052ce2
...
aa2aaaa4bb
Author | SHA1 | Date | |
---|---|---|---|
aa2aaaa4bb | |||
|
107dbe59c2 | ||
a3f6ab1407 | |||
c3160d612b | |||
9a355a4ad8 | |||
|
b8433fc19b | ||
23a64d217f | |||
fa6124f4fd | |||
698eb51be9 | |||
bd2245b01d | |||
71d7626311 | |||
617d7c760a | |||
37e29f17f0 |
9 changed files with 250 additions and 33 deletions
BIN
.swp
BIN
.swp
Binary file not shown.
10
CITATION.cff
Normal file
10
CITATION.cff
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
cff-version: 1.2.0
|
||||||
|
message: "If you use this software, please cite it as below."
|
||||||
|
authors:
|
||||||
|
- family-names: "Scherr"
|
||||||
|
given-names: "Christoph Johannes"
|
||||||
|
orcid: "https://orcid.org/0000-0000-0000-0000"
|
||||||
|
title: "RustCommandLineCalculator"
|
||||||
|
version: 0.2.1
|
||||||
|
date-released: 2023-08-22
|
||||||
|
url: "https://github.com/PlexSheep/RustCommandLineCalculator"
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "rust_command_line_calculator"
|
name = "rust_command_line_calculator"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
authors = ["Christoph J. Scherr <software@cscherr.de>"]
|
||||||
license = "GPL3"
|
license = "GPL3"
|
||||||
|
|
18
README.md
18
README.md
|
@ -3,22 +3,8 @@ RustCommandLineCalcuator, or simply rclc for short is a fast, scriptable calcula
|
||||||
designed to run right in your shell. No more need to use the python shell, or ugly and bloated
|
designed to run right in your shell. No more need to use the python shell, or ugly and bloated
|
||||||
GUIs. Easily calculate complex formulas in your bash scripts.
|
GUIs. Easily calculate complex formulas in your bash scripts.
|
||||||
|
|
||||||
Currently, rclc's status is `indev`. This means that important major features are still missing
|
Currently, rclc's status is `alpha`. This means that important major features are still missing
|
||||||
and bugs might not only be possible but common.
|
and bugs might not only be possible but common.
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
Not yet recommended, but you can always compile rclc by yourself with `cargo build` and copy the
|
rclc is still in an early version, but if you wish, you can compile and install it using `cargo install --path .`, this will copy a release version to `$HOME/.cargo/bin`. Otherweise, you can compile rclc manually using `cargo build --release` and copy the binary in `target/release/rclc` to a directory of your choice.
|
||||||
compiled binary executable to `/usr/local/bin`.
|
|
||||||
|
|
||||||
# Compatability
|
|
||||||
| Supported OS | OS |
|
|
||||||
|--------------|-----------------------|
|
|
||||||
| Current | Gnu/Linux |
|
|
||||||
| Planned | Windows, OSX, FreeBSD |
|
|
||||||
| Not Planned | TempleOS |
|
|
||||||
|
|
||||||
| Supported Architectures | Arch |
|
|
||||||
|-------------------------|--------|
|
|
||||||
| Current | x86_64 |
|
|
||||||
| Planned | major arm |
|
|
||||||
| Not Planned | any legacy |
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{fmt, error::Error, num::IntErrorKind};
|
use std::fmt;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
pub mod shunting_yard;
|
pub mod shunting_yard;
|
||||||
|
@ -221,7 +221,8 @@ impl Expression {
|
||||||
}
|
}
|
||||||
stop_at = index;
|
stop_at = index;
|
||||||
}
|
}
|
||||||
dbg!(&stop_at);
|
#[cfg(debug_assertions)]
|
||||||
|
{dbg!(&stop_at);}
|
||||||
// needed for none task: '1 + (1 + 1)'
|
// needed for none task: '1 + (1 + 1)'
|
||||||
let fixup = if stop_at == 0 { 0 } else { 1 };
|
let fixup = if stop_at == 0 { 0 } else { 1 };
|
||||||
task_text_full = possible_task.clone()[..stop_at+ fixup].chars().rev().collect::<String>();
|
task_text_full = possible_task.clone()[..stop_at+ fixup].chars().rev().collect::<String>();
|
||||||
|
@ -284,23 +285,29 @@ impl Expression {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Could not calculate result of child expression '{}': {}",
|
"Could not calculate result of child expression '{}': {}",
|
||||||
child.text,
|
child.text,
|
||||||
"error placeholder TODO"
|
err
|
||||||
);
|
);
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[cfg(debug_assertions)]{
|
||||||
dbg!(&child.full_text);
|
dbg!(&child.full_text);
|
||||||
dbg!(&child_full_text);
|
dbg!(&child_full_text);
|
||||||
|
}
|
||||||
normalized_text = normalized_text.replace(child.full_text.as_str(), child_full_text.as_str());
|
normalized_text = normalized_text.replace(child.full_text.as_str(), child_full_text.as_str());
|
||||||
}
|
}
|
||||||
|
#[cfg(debug_assertions)]{
|
||||||
dbg!(&normalized_text);
|
dbg!(&normalized_text);
|
||||||
|
}
|
||||||
// TODO Shunting yards algorithm, as we now have only calculatable values left.
|
// TODO Shunting yards algorithm, as we now have only calculatable values left.
|
||||||
// Implement this as public module in shunting_yard.rs
|
// Implement this as public module in shunting_yard.rs
|
||||||
// self.result = MYRESULT
|
// self.result = MYRESULT
|
||||||
let rpn = shunting_yard::form_reverse_polish_notation(&normalized_text);
|
let rpn = shunting_yard::form_reverse_polish_notation(&normalized_text);
|
||||||
match rpn {
|
match rpn {
|
||||||
Ok(valid_rpn) => {
|
Ok(valid_rpn) => {
|
||||||
|
#[cfg(debug_assertions)]{
|
||||||
dbg!(&valid_rpn);
|
dbg!(&valid_rpn);
|
||||||
|
}
|
||||||
return shunting_yard::calc_reverse_polish_notation(valid_rpn);
|
return shunting_yard::calc_reverse_polish_notation(valid_rpn);
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Custom made implementation of the shunting yard algorithm.
|
* Custom made implementation of the shunting yard algorithm.
|
||||||
|
@ -17,6 +18,15 @@ enum Associativity {
|
||||||
Left
|
Left
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Associativity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Associativity::Right => write!(f, "Right"),
|
||||||
|
Associativity::Left => write!(f, "Left"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct Operator {
|
pub struct Operator {
|
||||||
character: char,
|
character: char,
|
||||||
|
@ -24,6 +34,16 @@ pub struct Operator {
|
||||||
associativity: Associativity
|
associativity: Associativity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Operator {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("Operator")
|
||||||
|
.field("character", &self.character)
|
||||||
|
.field("precedence", &self.precedence)
|
||||||
|
.field("associativity", &self.associativity)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn is_operator(c: char) -> bool {
|
pub fn is_operator(c: char) -> bool {
|
||||||
for op in OPERATORS {
|
for op in OPERATORS {
|
||||||
|
@ -58,19 +78,19 @@ const SUBTRACTION: Operator = Operator {
|
||||||
|
|
||||||
const MULTIPLICATION: Operator = Operator {
|
const MULTIPLICATION: Operator = Operator {
|
||||||
character: '*',
|
character: '*',
|
||||||
precedence: 2,
|
precedence: 3,
|
||||||
associativity: Associativity::Left
|
associativity: Associativity::Left
|
||||||
};
|
};
|
||||||
|
|
||||||
const DIVISION: Operator = Operator {
|
const DIVISION: Operator = Operator {
|
||||||
character: '/',
|
character: '/',
|
||||||
precedence: 2,
|
precedence: 3,
|
||||||
associativity: Associativity::Left
|
associativity: Associativity::Left
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXPONENTIATION: Operator = Operator {
|
const EXPONENTIATION: Operator = Operator {
|
||||||
character: '*',
|
character: '^',
|
||||||
precedence: 2,
|
precedence: 4,
|
||||||
associativity: Associativity::Right
|
associativity: Associativity::Right
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,8 +108,9 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
while !(input_queue.is_empty()) {
|
while !(input_queue.is_empty()) {
|
||||||
// read a token
|
// read a token
|
||||||
let token: char = input_queue.pop().unwrap();
|
let token: char = input_queue.pop().unwrap();
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&token);
|
dbg!(&token);
|
||||||
|
|
||||||
// if the token is:
|
// if the token is:
|
||||||
// a number:
|
// a number:
|
||||||
if token.is_numeric() | (token == '.') {
|
if token.is_numeric() | (token == '.') {
|
||||||
|
@ -122,25 +143,26 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
Some(valid_op) => valid_op,
|
Some(valid_op) => valid_op,
|
||||||
None => {panic!("Operator '{}' not found.", token);},
|
None => {panic!("Operator '{}' not found.", token);},
|
||||||
};
|
};
|
||||||
|
|
||||||
// while there is an operator o2 at the top of the stack
|
// while there is an operator o2 at the top of the stack
|
||||||
if !operator_stack.is_empty() {
|
if !operator_stack.is_empty() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&operator_stack);
|
dbg!(&operator_stack);
|
||||||
let o2 = match Operator::get_operator(*(operator_stack.clone().last().clone().unwrap())) {
|
let o2 = match Operator::get_operator(*(operator_stack.clone().last().clone().unwrap())) {
|
||||||
Some(valid_op) => valid_op,
|
Some(valid_op) => valid_op,
|
||||||
None => {panic!("Operator '{}' not found.", token);},
|
None => {panic!("Operator '{}' not found.", token);},
|
||||||
};
|
};
|
||||||
// and
|
// and
|
||||||
// (o2 has greater precedence than o1 or (o1 and o2 have the same precedence and o1
|
// (o2 has greater precedence than o1 or (o1 and o2 have the same precedence and o1
|
||||||
// is left associative))
|
// is left associative))
|
||||||
while ((operator_stack.last().is_some()) & ((o2.precedence > o1.precedence) | ((o1.precedence == o2.precedence) & (o1.associativity == Associativity::Left)))) {
|
while (operator_stack.last().is_some()) & ((o2.precedence > o1.precedence) | ((o1.precedence == o2.precedence) & (o1.associativity == Associativity::Left))) {
|
||||||
// pop o2 from the operator stack into the output queue.
|
// pop o2 from the operator stack into the output queue.
|
||||||
// after this debug statement, the operator_stack is empty for no reason!!!!
|
// after this debug statement, the operator_stack is empty for no reason!!!!
|
||||||
// FIXME
|
// FIXME
|
||||||
let my_c = match operator_stack.pop() {
|
let my_c = match operator_stack.pop() {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => {panic!("weirdly gone!")},
|
None => {panic!("weirdly gone!")},
|
||||||
};
|
};
|
||||||
output_queue.push(vec![my_c]);
|
output_queue.push(vec![my_c]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,10 +171,10 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
/*
|
/*
|
||||||
// Unnessecary, will be processed by the expression parser
|
// Unnessecary, will be processed by the expression parser
|
||||||
else if '(' == token {
|
else if '(' == token {
|
||||||
println!("(");
|
println!("(");
|
||||||
}
|
}
|
||||||
else if ')' == token {
|
else if ')' == token {
|
||||||
println!(")");
|
println!(")");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
else {
|
else {
|
||||||
|
@ -163,6 +185,7 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
if currently_processing_numeric_group {
|
if currently_processing_numeric_group {
|
||||||
output_queue.push(current_numeric_group);
|
output_queue.push(current_numeric_group);
|
||||||
}
|
}
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&output_queue);
|
dbg!(&output_queue);
|
||||||
|
|
||||||
// afterwards, process any operators still on the operator_stack
|
// afterwards, process any operators still on the operator_stack
|
||||||
|
@ -170,6 +193,7 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
output_queue.push(vec![operator_stack.pop().unwrap()]);
|
output_queue.push(vec![operator_stack.pop().unwrap()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&output_queue);
|
dbg!(&output_queue);
|
||||||
let mut rpn: Vec<String> = Vec::new();
|
let mut rpn: Vec<String> = Vec::new();
|
||||||
for group in output_queue {
|
for group in output_queue {
|
||||||
|
@ -180,5 +204,95 @@ pub fn form_reverse_polish_notation(regular_math: &str) -> Result<Vec<String>, S
|
||||||
|
|
||||||
// after we have the rpn, we may want to calculate the values with it.
|
// after we have the rpn, we may want to calculate the values with it.
|
||||||
pub fn calc_reverse_polish_notation(rpn: Vec<String>) -> Result<f64, String> {
|
pub fn calc_reverse_polish_notation(rpn: Vec<String>) -> Result<f64, String> {
|
||||||
Ok(0.0)
|
|
||||||
|
// # function to evaluate reverse polish notation
|
||||||
|
// def evaluate(expression):
|
||||||
|
// # splitting expression at whitespaces
|
||||||
|
// expression = expression.split()
|
||||||
|
// # stack
|
||||||
|
// stack = []
|
||||||
|
// # iterating expression
|
||||||
|
// for ele in expression:
|
||||||
|
// # ele is a number
|
||||||
|
// if ele not in '/*+-':
|
||||||
|
// stack.append(int(ele))
|
||||||
|
// # ele is an operator
|
||||||
|
// else:
|
||||||
|
// # getting operands
|
||||||
|
// right = stack.pop()
|
||||||
|
// left = stack.pop()
|
||||||
|
// # performing operation according to operator
|
||||||
|
// if ele == '+':
|
||||||
|
// stack.append(left + right)
|
||||||
|
// elif ele == '-':
|
||||||
|
// stack.append(left - right)
|
||||||
|
// elif ele == '*':
|
||||||
|
// stack.append(left * right)
|
||||||
|
// elif ele == '/':
|
||||||
|
// stack.append(int(left / right))
|
||||||
|
// # return final answer.
|
||||||
|
// return stack.pop()
|
||||||
|
let mut stack: Vec<f64> = Vec::new();
|
||||||
|
|
||||||
|
for group in rpn {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dbg!(&group);
|
||||||
|
// find out what the group is, an operator, a number, or a variable.
|
||||||
|
// TODO add variables
|
||||||
|
if !Operator::is_operator(group.chars().last().unwrap()) {
|
||||||
|
let possible_num = group.parse::<f64>();
|
||||||
|
match possible_num {
|
||||||
|
Ok(valid) => {stack.push(valid);},
|
||||||
|
Err(_whatever) => {
|
||||||
|
eprint!("weird error happened, ending process...");
|
||||||
|
std::process::exit(2);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dbg!(&stack);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let op: Operator = Operator::get_operator(group.chars().last().unwrap()).unwrap();
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dbg!(&op);
|
||||||
|
let right = stack.pop().unwrap();
|
||||||
|
let left = stack.pop().unwrap();
|
||||||
|
|
||||||
|
if op == ADDITION {
|
||||||
|
stack.push(left + right);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if op == SUBTRACTION {
|
||||||
|
stack.push(left - right);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if op == MULTIPLICATION {
|
||||||
|
stack.push(left * right);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if op == DIVISION {
|
||||||
|
stack.push(left / right);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if op == EXPONENTIATION {
|
||||||
|
stack.push(left.powf(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if stack.is_empty() {
|
||||||
|
return Err("result stack empty".to_string());
|
||||||
|
}
|
||||||
|
if stack.len() > 1 {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
dbg!(stack);
|
||||||
|
return Err("result stack has too many results.".to_string());
|
||||||
|
}
|
||||||
|
return Ok(stack[0]);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
7
src/lib.rs
Normal file
7
src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Make module public
|
||||||
|
pub mod expression_parser;
|
||||||
|
|
||||||
|
// Make the function available at the root of the crate
|
||||||
|
pub use expression_parser::*;
|
||||||
|
|
||||||
|
|
1
test.txt
1
test.txt
|
@ -1 +0,0 @@
|
||||||
13 + 2525 + sqrt(15 + log_10(100)) + power_10(10)
|
|
94
tests/test.rs
Normal file
94
tests/test.rs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
use rust_command_line_calculator as rclc;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tests_are_loaded() {
|
||||||
|
assert_eq!("AA", "AA");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_sum_simple() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("40 + 33"),
|
||||||
|
String::from("40 + 33"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 73.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_sum_chain() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("20340 + 32424 + 24 + 23"),
|
||||||
|
String::from("20340 + 32424 + 24 + 23"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 20340.0 + 32424.0 + 24.0 + 23.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_difference_simple() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("33-13"),
|
||||||
|
String::from("33-13"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 33.0 - 13.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_difference_chain() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("353535 - 2405 - 33 - 13 - 4"),
|
||||||
|
String::from("353535 - 2405 - 33 - 13 - 4"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 353535.0 - 2405.0 - 33.0 - 13.0 - 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_product_simple() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("353* 13"),
|
||||||
|
String::from("353* 13"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 353.0 * 13.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_procuct_chain() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("353535 * 2405 * 33 * 13 * 4"),
|
||||||
|
String::from("353535 * 2405 * 33 * 13 * 4"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 353535.0 * 2405.0 * 33.0 * 13.0 * 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_quotient_simple() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("353 / 13"),
|
||||||
|
String::from("353 / 13"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 353.0 / 13.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_main_quotient_chain() {
|
||||||
|
let my_expression: rclc::Expression =
|
||||||
|
rclc::Expression::new(
|
||||||
|
String::from("353535 / 2405 / 33 / 13 / 4"),
|
||||||
|
String::from("353535 / 2405 / 33 / 13 / 4"),
|
||||||
|
rclc::Task::None,
|
||||||
|
0);
|
||||||
|
assert_eq!(my_expression.process().unwrap(), 353535.0 / 2405.0 / 33.0 / 13.0 / 4.0);
|
||||||
|
}
|
Reference in a new issue