diff --git a/cmake/modules/Notepad--.cmake b/cmake/modules/Notepad--.cmake index 510eb494638f5e725b1107b3bdbccf6f024a92f0..2ec6656933ad90e54a3a2b26e0883d7307ec4e80 100644 --- a/cmake/modules/Notepad--.cmake +++ b/cmake/modules/Notepad--.cmake @@ -1,7 +1,7 @@ # Notepad--.cmake # Notepad-- 核心构建 -# 在模块化构建中,这个部分代表着构建 Notepad-- +# 在模块化构建中,这个部分代表着构建 Notepad-- # 1. 默认构建时产出的目标为 Notepad-- # 2. 在此处可对 Notepad-- 目标进行详细的构建计划 @@ -11,11 +11,14 @@ if(TRUE) spark_aux_source_paths(CCEditorSources src src/cceditor + src/tinyexpr-plusplus ) spark_add_executable(${PROJECT_NAME} ${CCEditorSources} ${QRC_SOURCES}) + target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/cceditor + ${PROJECT_SOURCE_DIR}/src/tinyexpr-plusplus ${PROJECT_SOURCE_DIR}/src/qscint/src ${PROJECT_SOURCE_DIR}/src/qscint/src/Qsci @@ -44,15 +47,15 @@ if(TRUE) endif(TRUE) -# Notepad-- 目标在构建时依赖了一些其它内容,像先前构建的 QSci 目标、Qt5::XmlPatterns +# Notepad-- 目标在构建时依赖了一些其它内容,像先前构建的 QSci 目标、Qt5::XmlPatterns # Notepad-- 程序构建...配置 # ----------------- Notepad-- 构建宏支持相关 ----------------- # if(WIN32) # 在 Windows 中构建时,需要关注此库的构建形式,QScintilla 应该以何种方式编译 - target_compile_definitions(${PROJECT_NAME} - PRIVATE + target_compile_definitions(${PROJECT_NAME} + PRIVATE NO_PLUGIN # 开启插件支持 QSCINTILLA_DLL # 目前在 Windows 中使用 QSci 库时应该采用 Q_DECL_IMPORT # 控制 QSCINTILLA_EXPORT 符号应为 Q_DECL_IMPORT @@ -61,8 +64,8 @@ endif(WIN32) if(UNIX) # 在 Windows 中构建时,需要关注此库的构建形式,QScintilla 应该以何种方式编译 - target_compile_definitions(${PROJECT_NAME} - PRIVATE + target_compile_definitions(${PROJECT_NAME} + PRIVATE NO_PLUGIN # 开启插件支持 ) endif(UNIX) diff --git a/src/RealCompare.pro b/src/RealCompare.pro index 377d744a665846ad0646c03ad7258c110cbbf792..afe3ca6decc1b760bbbe1c5ca052c54b0b3d0f18 100755 --- a/src/RealCompare.pro +++ b/src/RealCompare.pro @@ -10,14 +10,16 @@ QT += core gui widgets concurrent network xmlpatterns HEADERS += *.h \ cceditor/ccnotepad.h \ - cceditor/filemanager.h + cceditor/filemanager.h \ + tinyexpr-plusplus/tinyexpr.h + - SOURCES += *.cpp \ cceditor/ccnotepad.cpp \ - cceditor/filemanager.cpp + cceditor/filemanager.cpp \ + tinyexpr-plusplus/tinyexpr.cpp + - FORMS += *.ui \ cceditor/ccnotepad.ui @@ -33,6 +35,8 @@ INCLUDEPATH += cceditor TRANSLATIONS += realcompare_zh.ts +QMAKE_CXXFLAGS += -std=c++17 + if(contains(QMAKE_HOST.arch, x86_64|loongarch64)){ CONFIG(Debug, Debug|Release){ DESTDIR = x64/Debug diff --git a/src/cceditor/ccnotepad.cpp b/src/cceditor/ccnotepad.cpp index 0f27530b72b24e090736d5746d4ef4c5c9b71c78..6e20d72a91f8fde05b686abd147d74da4e59ddcb 100755 --- a/src/cceditor/ccnotepad.cpp +++ b/src/cceditor/ccnotepad.cpp @@ -41,9 +41,9 @@ #include #include #include -#include +#include #include -#include +#include #include #include #include @@ -1285,19 +1285,22 @@ void CCNotePad::quickshow() m_langDescLabel = new QLabel("Txt", ui.statusBar); m_zoomLabel = new QLabel("Zoom", ui.statusBar); + m_jitEvalLabel = new QLabel("", ui.statusBar); m_codeStatusLabel->setMinimumWidth(120); m_lineEndLabel->setMinimumWidth(100); m_lineNumLabel->setMinimumWidth(120); m_langDescLabel->setMinimumWidth(100); m_zoomLabel->setMinimumWidth(100); + m_jitEvalLabel->setMinimumWidth(30); //0在前面,越小越在左边 - ui.statusBar->insertPermanentWidget(0, m_zoomLabel); - ui.statusBar->insertPermanentWidget(1, m_langDescLabel); - ui.statusBar->insertPermanentWidget(2, m_lineNumLabel); - ui.statusBar->insertPermanentWidget(3, m_lineEndLabel); - ui.statusBar->insertPermanentWidget(4, m_codeStatusLabel); + ui.statusBar->insertPermanentWidget(0, m_jitEvalLabel); + ui.statusBar->insertPermanentWidget(1, m_zoomLabel); + ui.statusBar->insertPermanentWidget(2, m_langDescLabel); + ui.statusBar->insertPermanentWidget(3, m_lineNumLabel); + ui.statusBar->insertPermanentWidget(4, m_lineEndLabel); + ui.statusBar->insertPermanentWidget(5, m_codeStatusLabel); initToolBar(); @@ -1921,7 +1924,7 @@ void CCNotePad::slot_batchFind() { adjustWInPos(m_batchFindWin); } -#endif +#endif } @@ -2594,7 +2597,7 @@ void CCNotePad::slot_showCmdInExplorer() { ui.statusBar->showMessage(tr("open cmd in file dir %1 failed.").arg(dirEment)); } -#if 0 +#if 0 //下面方法不能分离式,主界面关闭后,cmd也消失了 QStringList arguments; arguments << "/K"; @@ -3308,7 +3311,7 @@ void CCNotePad::initToolBar() m_pLexerActGroup->addAction(ui.actionAviSynth); m_pLexerActGroup->addAction(ui.actionASN1); m_pLexerActGroup->addAction(ui.actionSwift); - m_pLexerActGroup->addAction(ui.actionIntel_HEX); + m_pLexerActGroup->addAction(ui.actionIntel_HEX); m_pLexerActGroup->addAction(ui.actionGo); m_pLexerActGroup->addAction(ui.actionTxt); m_pLexerActGroup->addAction(ui.actionUserDefine); @@ -3319,6 +3322,28 @@ void CCNotePad::initToolBar() connect(ui.menuLanguage, &QMenu::triggered, this, &CCNotePad::slot_lexerActTrig); + m_pEvalAccuracy = new QActionGroup(this); + auto uiacndp = QVector{ui.action1_decimal_places, ui.action2_decimal_places, ui.action3_decimal_places, + ui.action4_decimal_places, ui.action5_decimal_places, ui.action6_decimal_places, ui.action7_decimal_places, + ui.action8_decimal_places, ui.action9_decimal_places, ui.action10_decimal_places, ui.action11_decimal_places, + ui.action12_decimal_places, ui.action13_decimal_places, ui.action14_decimal_places, ui.action15_decimal_places + }; + const int evalaccu = NddSetting::getKeyValueFromNumSets(EVAL_ACCURACY); + int i = 1; //C++20才支持在范围for中定义在其作用域的额外变量 + for (auto & act: uiacndp) { + act->setChecked(bool(i==evalaccu)); + m_pEvalAccuracy->addAction(act)->setData(QVariant(i++)); + } + m_pEvalAccuracy->setExclusive(true); + connect(m_pEvalAccuracy, &QActionGroup::triggered, this, &CCNotePad::slot_eval_accuracy, Qt::QueuedConnection); + + ui.actionEnterEval->setChecked(bool(NddSetting::getKeyValueFromNumSets(ENTER_EVAL))); + ui.actionQuestionEval->setChecked(bool(NddSetting::getKeyValueFromNumSets(QUESTION_EVAL))); + const auto enablejit = bool(NddSetting::getKeyValueFromNumSets(JIT_EVAL)); + ui.actionJIT_eval_on_status_bar->setChecked(enablejit); + if( m_jitEvalLabel != nullptr ) + m_jitEvalLabel->setVisible(enablejit); + //这是在网上看到的一个方法,使用一个widget把位置占住,让后面的action跑到最后面 去 QWidget* space = new QWidget(); space->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -3547,7 +3572,7 @@ int CCNotePad::findFileIsOpenAtPad(QString filePath) return ret; } -//判断新建名称是否已经存在,是 true +//判断新建名称是否已经存在,是 true bool CCNotePad::isNewFileNameExist(QString& fileName) { @@ -3688,7 +3713,7 @@ void CCNotePad::on_roladFile(ScintillaEditView* pEdit,quint64 lastSize, qint64 c } #endif -void CCNotePad::doReloadTxtFile(ScintillaEditView* pEdit, bool isOnTail, qint64 startReadSize) +void CCNotePad::doReloadTxtFile(ScintillaEditView* pEdit, bool isOnTail, qint64 startReadSize) { //reloadEditFile 里面会关闭和新增tab,触发一系列的currentChanged disconnect(ui.editTabWidget, &QTabWidget::currentChanged, this, &CCNotePad::slot_tabCurrentChanged); @@ -3757,7 +3782,7 @@ bool CCNotePad::checkRoladFile(ScintillaEditView* pEdit, qint64 startReadSize) } } - else + else { QString filePath = pEdit->property(Edit_View_FilePath).toString(); @@ -5772,7 +5797,7 @@ void CCNotePad::slot_actionRenameFile_toggle(bool checked) if (oldName == fileName) { return; - } + } QFileInfo newfi(fileName); @@ -7506,7 +7531,7 @@ void CCNotePad::on_findResultlineDoubleClick(QString* pFilePath, int pos, int en if (pCurEdit != nullptr) { pCurEdit->execute(SCI_SETSEL, pos, end); - } + } } #if 0 @@ -9792,7 +9817,7 @@ void CCNotePad::slot_formatXml() } } - if (reader.hasError()) + if (reader.hasError()) { ui.statusBar->showMessage(tr("XML format error, please check!"), MSG_EXIST_TIME); QApplication::beep(); @@ -9952,6 +9977,33 @@ void CCNotePad::slot_shortcutManager() pWin->show(); } +//表达式求值功能开关 +void CCNotePad::slot_enter_eval(bool check) +{ + NddSetting::updataKeyValueFromNumSets(ENTER_EVAL, check?1:0); +} +void CCNotePad::slot_question_eval(bool check) +{ + NddSetting::updataKeyValueFromNumSets(QUESTION_EVAL, check?1:0); +} +void CCNotePad::slot_jit_eval(bool check) +{ + NddSetting::updataKeyValueFromNumSets(JIT_EVAL, check?1:0); + m_jitEvalLabel->setVisible(check); +} + +//表达式求值精度 +void CCNotePad::slot_eval_accuracy(QAction *action) +{ + NddSetting::updataKeyValueFromNumSets(EVAL_ACCURACY, action->data().toInt()); +} + +//表达式及时计算结果显示在状态栏 +void CCNotePad::set_eval_jit_value(QString val) +{ + m_jitEvalLabel->setText(val); +} + //处理当前按下ESC后,需要处理退出的事件 void CCNotePad::on_quitActiveWindow() { @@ -9973,7 +10025,7 @@ void CCNotePad::on_quitActiveWindow() } } -#if 0 +#if 0 //修改主题颜色//暂时不开始,发现MAC下有不开启深色的配置 void CCNotePad::changeAppFontColor(QColor color) { diff --git a/src/cceditor/ccnotepad.h b/src/cceditor/ccnotepad.h index ec8fb2324d6916ec61208fb0268ef8ada351cb29..d08b7cf76177ea2c7a6a40b1dfeac92cd0856422 100755 --- a/src/cceditor/ccnotepad.h +++ b/src/cceditor/ccnotepad.h @@ -149,6 +149,8 @@ public: void changeMarkColor(int sytleId); void setUserDefShortcutKey(int shortcutId); + void set_eval_jit_value(QString val); + QtLangSet* getLangSet(); void setEditLangs(ScintillaEditView* pEdit,LangType langs); #ifdef NO_PLUGIN @@ -161,7 +163,7 @@ signals: void signLinkNetServer(); #ifdef Q_OS_WIN void tailFileChange(ScintillaEditView*,qint64 lastSize, qint64 curSize); -#endif +#endif public slots: void slot_changeChinese(); void slot_changeEnglish(); @@ -364,6 +366,10 @@ private slots: void slot_showWebAddr(bool check); void slot_langFileSuffix(); void slot_shortcutManager(); + void slot_enter_eval(bool check); + void slot_question_eval(bool check); + void slot_jit_eval(bool check); + void slot_eval_accuracy(QAction * action); void on_lineEndChange(int index); void on_tailfile(bool isOn); #ifdef Q_OS_WIN @@ -498,6 +504,7 @@ private: QLabel* m_lineNumLabel; QLabel* m_langDescLabel; QLabel* m_zoomLabel; + QLabel* m_jitEvalLabel; QMenu* m_tabRightClickMenu; @@ -513,6 +520,7 @@ private: QActionGroup *m_pLineEndActGroup; QActionGroup *m_pLexerActGroup; QActionGroup *m_pIconSize; + QActionGroup *m_pEvalAccuracy; #if 0 QAction* m_quitAction; diff --git a/src/cceditor/ccnotepad.ui b/src/cceditor/ccnotepad.ui index ddb12f81378c5c5bb052911614f4e552e6147872..2c2e67123bcc9ca33737fec440e5667398dc1de0 100755 --- a/src/cceditor/ccnotepad.ui +++ b/src/cceditor/ccnotepad.ui @@ -482,12 +482,42 @@ + + + Expression evaluation + + + + Evaluation Accuracy + + + + + + + + + + + + + + + + + + + + + + + @@ -2069,6 +2099,162 @@ Md5/Sha + + + true + + + true + + + Use Enter key + + + + + true + + + true + + + Use ? key + + + + + true + + + 1 decimal places + + + + + true + + + 2 decimal places + + + + + true + + + 3 decimal places + + + + + true + + + 4 decimal places + + + + + true + + + 5 decimal places + + + + + true + + + true + + + 6 decimal places + + + + + true + + + 7 decimal places + + + + + true + + + 8 decimal places + + + + + true + + + 9 decimal places + + + + + true + + + 10 decimal places + + + + + true + + + 11 decimal places + + + + + true + + + 12 decimal places + + + + + true + + + 13 decimal places + + + + + true + + + 14 decimal places + + + + + true + + + 15 decimal places + + + + + true + + + true + + + JIT eval on status bar + + @@ -3499,6 +3685,54 @@ + + actionEnterEval + triggered(bool) + CCNotePad + slot_enter_eval(bool) + + + -1 + -1 + + + 728 + 394 + + + + + actionQuestionEval + triggered(bool) + CCNotePad + slot_question_eval(bool) + + + -1 + -1 + + + 728 + 394 + + + + + actionJIT_eval_on_status_bar + triggered(bool) + CCNotePad + slot_jit_eval(bool) + + + -1 + -1 + + + 728 + 394 + + + slot_actionNewFile_toggle(bool) @@ -3602,6 +3836,9 @@ slot_showWebAddr(bool) slot_langFileSuffix() slot_shortcutManager() + slot_enter_eval(bool) + slot_question_eval(bool) + slot_jit_eval(bool) on_md5hash() diff --git a/src/macpro/RealCompare.pro b/src/macpro/RealCompare.pro index 065075705693407f8123a4c65ee553c9125ce78a..24c2ed0e644a58034feab561d930e91eedfd34e6 100755 --- a/src/macpro/RealCompare.pro +++ b/src/macpro/RealCompare.pro @@ -9,12 +9,14 @@ QT += core gui widgets concurrent network macextras HEADERS += *.h \ cceditor/ccnotepad.h \ - cceditor/filemanager.h - + cceditor/filemanager.h \ + tinyexpr-plusplus/tinyexpr.h + SOURCES += *.cpp *.cc \ cceditor/ccnotepad.cpp \ - cceditor/filemanager.cpp - + cceditor/filemanager.cpp \ + tinyexpr-plusplus/tinyexpr.cpp + FORMS += *.ui \ cceditor/ccnotepad.ui @@ -28,7 +30,9 @@ INCLUDEPATH += cceditor DEFINES += QSCINTILLA_DLL TRANSLATIONS += realcompare_zh.ts - + +QMAKE_CXXFLAGS += -std=c++17 + win32 { if(contains(QMAKE_HOST.arch, x86_64)){ CONFIG(Debug, Debug|Release){ diff --git a/src/nddsetting.cpp b/src/nddsetting.cpp index f4826a73b8a8fe147aaab621b3b8cb5f6d5e66e3..4767932e39543d6391c09b9a6ca61ea0a0206bfc 100755 --- a/src/nddsetting.cpp +++ b/src/nddsetting.cpp @@ -104,7 +104,7 @@ void NddSetting::init() addKeyValueToNumSets(ICON_SIZE, 1); addKeyValueToNumSets(ZOOMVALUE, 100); - + addKeyValueToNumSets(FINDRESULTPOS, Qt::BottomDockWidgetArea); addKeyValueToNumSets(FILELISTPOS, Qt::LeftDockWidgetArea); @@ -120,6 +120,12 @@ void NddSetting::init() //查找结果框的默认字体大小 addKeyValueToNumSets(FIND_RESULT_FONT_SIZE, 14); + + //表达式求值 + addKeyValueToNumSets(ENTER_EVAL, 1); + addKeyValueToNumSets(QUESTION_EVAL, 1); + addKeyValueToNumSets(JIT_EVAL, 1); + addKeyValueToNumSets(EVAL_ACCURACY, 6); }; if (!s_nddSet->contains(VERSION)) @@ -174,7 +180,7 @@ void NddSetting::init() checkNoExistAdd(SHOWSPACE_KEY, v); } - + { QVariant v(100); checkNoExistAdd(MAX_BIG_TEXT, v); @@ -233,6 +239,22 @@ void NddSetting::init() QVariant v(0); checkNoExistAdd(LAST_ACTION_TAB_INDEX, v); } + { + QVariant v(1); + checkNoExistAdd(ENTER_EVAL, v); + } + { + QVariant v(1); + checkNoExistAdd(QUESTION_EVAL, v); + } + { + QVariant v(1); + checkNoExistAdd(JIT_EVAL, v); + } + { + QVariant v(6); + checkNoExistAdd(EVAL_ACCURACY, v); + } } while (false); } @@ -312,7 +334,7 @@ void NddSetting::close() s_nddSet->sync(); delete s_nddSet; s_nddSet = nullptr; - s_isContentChanged = false; + s_isContentChanged = false; } //在这里保存一下子窗口的位置。不排除有可能子窗口还在,主窗口已经退出的情况,不过问题不大。 diff --git a/src/nddsetting.h b/src/nddsetting.h index 6a65690304ade194d06d503511f6c65f3329dc7a..ee69a03923d5178072e97f135e269455c4c04111 100755 --- a/src/nddsetting.h +++ b/src/nddsetting.h @@ -30,6 +30,11 @@ static QString RECENT_OPEN_FILE = "recentopenfile"; static QString LAST_OPEN_DIR = "lastdir"; static QString CLEAR_OPENFILE_ON_CLOSE = "clearopenfile"; //关闭时清空历史文件 +static QString ENTER_EVAL = "entereval"; //是否启用回车键求值 +static QString QUESTION_EVAL = "questioneval"; //是否启用问号键求值 +static QString JIT_EVAL = "jiteval"; //是否启用及时求值并显示在状态栏 +static QString EVAL_ACCURACY = "evalaccuracy"; //求值浮点精度 + //下面这个是winpos.ini中的key,避免单个文件太大,拖慢启动速度 static QString BATCH_FIND_REPLACE_POS = "bfpos";//批量查找替换窗口的大小 diff --git a/src/realcompare_zh.qm b/src/realcompare_zh.qm index fc26ea997ecb9af29c407456a4e860891e926486..cb2cfc1ef6a2564c2098a9f135f822abc33be32e 100755 Binary files a/src/realcompare_zh.qm and b/src/realcompare_zh.qm differ diff --git a/src/realcompare_zh.ts b/src/realcompare_zh.ts index 64fdab49765c033280144f4dda69ffaf57d4234d..0c32faeecf95e48e0c8185ef58dae14d084dcdf6 100755 --- a/src/realcompare_zh.ts +++ b/src/realcompare_zh.ts @@ -482,6 +482,16 @@ G + + + Expression evaluation + 表达式求值(计算器) + + + + Evaluation Accuracy + 浮点数精度 + @@ -1537,6 +1547,96 @@ Verilog + + + Use Enter key + =号后按回车键计算 + + + + Use ? key + =号后按?问号键计算 + + + + JIT eval on status bar + 即时计算(状态栏显示) + + + + 1 decimal places + 1位 + + + + 2 decimal places + 2位 + + + + 3 decimal places + 3位 + + + + 4 decimal places + 4位 + + + + 5 decimal places + 5位 + + + + 6 decimal places + 6位 + + + + 7 decimal places + 7位 + + + + 8 decimal places + 8位 + + + + 9 decimal places + 9位 + + + + 10 decimal places + 10位 + + + + 11 decimal places + 11位 + + + + 12 decimal places + 12位 + + + + 13 decimal places + 13位 + + + + 14 decimal places + 14位 + + + + 15 decimal places + 15位 + @@ -2351,9 +2451,9 @@ - File %1 + File %1 File Size %2 > %3M, How to Open it ? - 文件 %1 + 文件 %1 文件大小 %2 大于 %3M,请选择打开方式。 @@ -2575,7 +2675,7 @@ File Size %2 > %3M, How to Open it ? - Save File %1 failed. You may not have write privileges + Save File %1 failed. You may not have write privileges Please save as a new file! 保存文件 %1 失败! 你可能没有文件写权限,请另存为一个新文件! @@ -2601,7 +2701,7 @@ Please save as a new file! "%1" - + This file has been modified by another program. Do you want to reload it? %1\n\n \n文件已在外部被其它程序修改。\n是否重新加载该文件? @@ -2898,7 +2998,7 @@ Do you want to reload it? 当前是常规文本文档,不能使用该功能! - bugfix: https://github.com/cxasm/notepad-- + bugfix: https://github.com/cxasm/notepad-- china: https://gitee.com/cxasm/notepad-- bugfix: https://github.com/cxasm/notepad-- bug反馈:https://github.com/cxasm/notepad-- @@ -3628,11 +3728,11 @@ please wait ... - Left size %1 byte, right size %2 byte -Equal content size %3 + Left size %1 byte, right size %2 byte +Equal content size %3 Left Equal ratio %4 Right Equal ratio %5 左文件大小 %1 右文件大小 %2 -相等内容长度 %3 +相等内容长度 %3 左边相等率 %4 右边相等率%5 @@ -5202,14 +5302,14 @@ file size %1, please wait ... - Find All in Current + Find All in Current Document 在当前文件中查找 - Find All in All Opened + Find All in All Opened Documents 查找所有打开文件 @@ -5250,7 +5350,7 @@ file size %1, please wait ... - Replace All in All Opened + Replace All in All Opened Documents 替换所有打开文件 @@ -5685,7 +5785,7 @@ file size %1, please wait ... - Max Bin File Size is 10M ! Exceeding file size ! + Max Bin File Size is 10M ! Exceeding file size ! Select a shorter range for comparison. 文件最大对比长度为10M! 当前文件超过最大限制。 请选择一个文件范围来进行对比。 @@ -5847,7 +5947,7 @@ Select a shorter range for comparison. LangExtSet - current lang: %1 + current lang: %1 ext file suffix is : %2 Double-click a column item to modify the syntax association file. 当前语言:%1 @@ -7487,8 +7587,8 @@ Double-click a column item to modify the syntax association file. - Current Select Word Nums is %1 . -Line nums is %2 . + Current Select Word Nums is %1 . +Line nums is %2 . Space nums is %3, Non-space is %4 . 当前选择文本字符数量是 %1 行数量是 %2 @@ -7496,8 +7596,8 @@ Space nums is %3, Non-space is %4 . - Current Doc Word Nums is %1 . -Line nums is %2 . + Current Doc Word Nums is %1 . +Line nums is %2 . Space nums is %3, Non-space is %4 . 当前文本字符数量是 %1 行数量是 %2 diff --git a/src/scintillaeditview.cpp b/src/scintillaeditview.cpp index ef6ceacdd82f899ee7acf2fe2f0e59d0e3213579..eafaf0d4e8470cdb62f4af1fc760a14c1924ddab 100755 --- a/src/scintillaeditview.cpp +++ b/src/scintillaeditview.cpp @@ -9,6 +9,7 @@ #include "filemanager.h" #include "shortcutkeymgr.h" #include "markdownview.h" +#include "nddsetting.h" #include #include @@ -67,15 +68,17 @@ #include #include #include +#include #include #include +#include // initialize the static variable -#define DEFAULT_FONT_NAME "Courier New" //"Microsoft YaHei" +#define DEFAULT_FONT_NAME "Courier New" //"Microsoft YaHei" int ScintillaEditView::s_tabLens = 4; @@ -244,8 +247,182 @@ ScintillaEditView* ScintillaEditView::createEditForSearch() return new ScintillaEditView(); } +//tinyexpr++只支持数值运算并返回双精度浮点结果,未支持时间表达式加减法,在此单独简单实现 +//时间表达式不支持即时求值,只能在输入完整表达式和=等号后,按回车或?号计算时间差或者和 +//经常进行视频或者音频编辑的行业用户适用此功能,计算影片裁剪长度等用途 +static inline const std::map s_timeFormat = { +{"^(\\d{1,2}):(\\d{1,2})\\.(\\d{1,3})$", "m:s.z"}, +{"^(\\d{1,2}):(\\d{1,2}):(\\d{1,2})$", "h:m:s"}, +{"^(\\d{1,2}):(\\d{1,2}):(\\d{1,2})\\.(\\d{1,3})$", "h:m:s.z"}, +{"^(\\d{1,2})h(\\d{1,2})m$", "h'h'm'm'"}, +{"^(\\d{1,2})m(\\d{1,2})s$", "m'm's's'"}, +{"^(\\d{1,2})m(\\d{1,2})$", "m'm's"}, +{"^(\\d{1,2})m(\\d{1,2})\\.(\\d{1,3})$", "m'm's.z"}, +{"^(\\d{1,2})m(\\d{1,2})s(\\d{1,3})$", "m'm's's'z"}, +{"^(\\d{1,2})m(\\d{1,2})s\\.(\\d{1,3})$", "m'm's's'.z"}, +{"^(\\d{1,7})s$", "s's'"}, +{"^(\\d{1,7})$", "s"}, +{"^(\\d{1,7})s(\\d{1,3})$", "s's'z"}, +{"^(\\d{1,7})s\\.(\\d{1,3})$", "s's'.z"}, +{"^(\\d{1,7})\\.(\\d{1,3})$", "s.z"}, +{"^(\\d{1,2})h(\\d{1,2})m(\\d{1,2})$", "h'h'm'm's"}, +{"^(\\d{1,2})h(\\d{1,2})m(\\d{1,2})s$", "h'h'm'm's's'"}, +{"^(\\d{1,2})h(\\d{1,2})m(\\d{1,2})\\.(\\d{1,3})$", "h'h'm'm's.z"}, +{"^(\\d{1,2})h(\\d{1,2})m(\\d{1,2})s\\.(\\d{1,3})$", "h'h'm'm's's'.z"} +}; +static inline const auto s_reTimeformat = QRegularExpression( + "^((\\d+[h:])?(\\d+[m:])?\\d+((s|\.|s\.)\\d+)?)([+\\-])([\\d:hms\\.]+)$"); +static inline const auto ztmfront = QRegularExpression("^[0:]*"); + +inline QString timetrim(const QString& intm) +{ + return QString(intm).remove(ztmfront); +} + +inline QString& removeTail0andDot(QString& s) +{ + if (s.contains(".")) { + while (s.back() == '0') s.chop(1); + if (s.back() == '.') s.chop(1); + } + return s; +} + +static inline bool timeExprCalc(const QString& expr, QString& retstr) { + QString tmstr = expr; + tmstr.replace(" ", ""); + auto m = s_reTimeformat.match(tmstr); + if (!m.hasMatch()) + return false; + auto tm1 = m.captured(1); + bool tmadd = bool(m.captured(6) == "+"); + auto tm2 = m.captured(7); + QTime qtm1; + QTime qtm2; +// for (int i = 1; i <= 7; ++i) +// qDebug() << i << m.captured(i); + bool allOK = false; + for (const auto& [key, val] : s_timeFormat) { + auto re1 = QRegularExpression(key); + if (!qtm1.isValid()) { + auto mtm = re1.match(tm1); + if (mtm.hasMatch()) { + qtm1 = QTime::fromString(tm1, val); + qDebug() << "time pat" << key << timetrim(qtm1.toString("h:m:s.z")); + } + } + auto re2 = QRegularExpression(key); + if (!qtm2.isValid()) { + auto mtm = re2.match(tm2); + if (mtm.hasMatch()) { + qtm2 = QTime::fromString(tm2, val); + qDebug() << "time pat" << key << timetrim(qtm2.toString("h:m:s.z")); + } + } + if (qtm1.isValid() && qtm2.isValid()) + { + allOK = true; + break; + } + } + if (!allOK) + return false; + QTime res = QTime::fromMSecsSinceStartOfDay(tmadd? + qtm1.msecsSinceStartOfDay() + qtm2.msecsSinceStartOfDay(): + qtm1.msecsSinceStartOfDay() - qtm2.msecsSinceStartOfDay() + ); + retstr = timetrim(res.toString("h:m:s.z")); + removeTail0andDot(retstr); + return true; +} + +bool ScintillaEditView::tinyexprCalc(evltype evt) { + int line, index; + getCursorPosition(&line, &index); + if (index <= 0) + return false; + int idx = index - 1; + QString linetxt = text(line); + if (linetxt.length() < 1) + return false; + switch(evt) + { + case EVAL_QUESTION: + if (!NddSetting::getKeyValueFromNumSets(QUESTION_EVAL)) + return false; + if (linetxt.at(idx) != '=') //只有光标左边是等号才计算 + return false; + break; + case EVAL_ENTER: + if (!NddSetting::getKeyValueFromNumSets(ENTER_EVAL)) + return false; + if (linetxt.at(idx) != '=') //只有光标左边是等号才计算 + return false; + break; + case EVAL_JIT: + default: + if (!NddSetting::getKeyValueFromNumSets(JIT_EVAL)) + return false; + ++idx; //即时计算时光标左边不是等号,无需跳过 + break; + } + const int eval_accuracy = NddSetting::getKeyValueFromNumSets(EVAL_ACCURACY); + QString exprline = linetxt.left(idx); + bool is_eq = false; + int i = 0; + //TODO:利用正规表达式似乎能简化这部分代码,然而不会写 + //循环从右向左看,第一个单独出现的=号作为表达式左边界,==属于表达式逻辑运算符被接受 + for (int ii = idx - 1; ii > -1; --ii) { + if (exprline.at(ii) == '=') { + is_eq = !is_eq; + } else if (is_eq) { + i = ii + 2; + break; + } + } + while (i < idx) { + auto expr = exprline.mid(i); + if (evt != EVAL_JIT) { + QString retstr; + if (timeExprCalc(expr, retstr)) { + insertAt(retstr, line, idx + 1); + setCursorPosition(line, idx + 1 + retstr.length()); + return true; + } + } + //VS2019默认C++14,tinyexpr++无外部依赖库但用了C++17语法,VC++需要启用C++17或20标准来编译 + auto v = m_tinyEpr.evaluate(expr.toStdString()); + if (!std::isnan(v)) { + QString res = QString::number(v, 'g', eval_accuracy); + if (res.contains('.')) //消小数点末尾无用0;只剩小数点也将之消除,显示为整数 + removeTail0andDot(res); + if(evt == EVAL_JIT) { + res = expr + "=" + res; + CCNotePad* pv = dynamic_cast(m_NoteWin); + if (pv != nullptr) + { + pv->set_eval_jit_value(res); + } + } else { + insertAt(res, line, idx + 1); + setCursorPosition(line, idx + 1 + res.length()); + } + //qDebug() << "tinyexpr: " << expr << " = " << res; + return true; + } + while (++i && i < idx - 1 && + !s_teSymbols.contains(exprline.at(i)) && + !(s_teLookfwdSymbols.contains(exprline.at(i)) && + exprline.at(i) == exprline.at(i+1)) + ) {} + } + return false; +} + //截获ESC键盘,让界面去退出当前的子界面 +//处理回车和问号,如果符合光标左边是等号条件,执行表达式求值 +//即时求值开启时,按数字、右圆括号、END、BACKSP按键时会求值并显示在状态栏 void ScintillaEditView::keyPressEvent(QKeyEvent* event) { switch (event->key()) @@ -256,10 +433,23 @@ void ScintillaEditView::keyPressEvent(QKeyEvent* event) m_NoteWin->on_quitActiveWindow(); } break; + case Qt::Key_Return: + case Qt::Key_Enter: + tinyexprCalc(EVAL_ENTER); + break; + case Qt::Key_Question: + if(tinyexprCalc(EVAL_QUESTION)) + return; //返回真值吃掉输入的?号不上屏,或者说?号被运算结果所替代 + case Qt::Key_0:case Qt::Key_1:case Qt::Key_2:case Qt::Key_3:case Qt::Key_4: + case Qt::Key_5:case Qt::Key_6:case Qt::Key_7:case Qt::Key_8:case Qt::Key_9: + case Qt::Key_ParenRight:case Qt::Key_Backspace:case Qt::Key_End: + QsciScintilla::keyPressEvent(event); + tinyexprCalc(EVAL_JIT); + return; default: break; } - return QsciScintilla::keyPressEvent(event); + QsciScintilla::keyPressEvent(event); } @@ -319,7 +509,7 @@ void ScintillaEditView::updateLineNumbersMargin(bool forcedToHide) { { execute(SCI_SETMARGINWIDTHN, _SC_MARGE_LINENUMBER, (sptr_t)0); } - else + else { updateLineNumberWidth(0); } @@ -370,7 +560,7 @@ void ScintillaEditView::updateLineNumberWidth(int lineNumberMarginDynamicWidth) auto pixelWidth = 6 + nbDigits * execute(SCI_TEXTWIDTH, STYLE_LINENUMBER, reinterpret_cast("8")); execute(SCI_SETMARGINWIDTHN, _SC_MARGE_LINENUMBER, pixelWidth); - + } } else @@ -378,7 +568,7 @@ void ScintillaEditView::updateLineNumberWidth(int lineNumberMarginDynamicWidth) int pixelWidth = 6 + INIT_BIG_RO_TEXT_LINE_WIDTH * execute(SCI_TEXTWIDTH, STYLE_LINENUMBER, reinterpret_cast("8")); execute(SCI_SETMARGINWIDTHN, SC_BIGTEXT_LINES, pixelWidth); } - + } @@ -440,7 +630,7 @@ QString ScintillaEditView::getTagByLexerId(int lexerId) case L_OBJC: return ("objc"); - + case L_CS: return ("csharp"); @@ -470,7 +660,7 @@ QString ScintillaEditView::getTagByLexerId(int lexerId) case L_SQL: return "sql"; - + case L_VB: return "vb"; @@ -655,7 +845,7 @@ QString ScintillaEditView::getTagByLexerId(int lexerId) default: break; } - + return ""; } @@ -989,7 +1179,7 @@ QList& ScintillaEditView::getCurMarkRecord() void ScintillaEditView::setIndentGuide(bool willBeShowed) { - QsciLexer* pLexer = this->lexer(); + QsciLexer* pLexer = this->lexer(); if (nullptr == pLexer) { return; @@ -1038,7 +1228,7 @@ void ScintillaEditView::init() setMarginSensitivity(_SC_MARGE_SYBOLE, true); connect(this, &QsciScintilla::marginClicked, this, &ScintillaEditView::slot_bookMarkClicked); - + //开始括号匹配,比如html的<>,开启前后这类字段的匹配 setBraceMatching(SloppyBraceMatch); @@ -1051,7 +1241,7 @@ void ScintillaEditView::init() QFont font(DEFAULT_FONT_NAME, 11, QFont::Normal); setFont(font); setMarginsFont(font); - + execute(SCI_SETTABWIDTH, ScintillaEditView::s_tabLens); //使用空格替换tab @@ -1060,7 +1250,7 @@ void ScintillaEditView::init() //这个无比要设置false,否则双击后高亮单词,拷贝时会拷贝多个选择。 execute(SCI_SETMULTIPLESELECTION, true); execute(SCI_SETADDITIONALSELECTIONTYPING, true); - + execute(SCI_SETMULTIPASTE, SC_MULTIPASTE_EACH); execute(SCI_SETADDITIONALCARETSVISIBLE, true); @@ -1117,7 +1307,7 @@ void ScintillaEditView::init() //execute(SCI_INDICSETFORE, SCE_UNIVERSAL_FOUND_STYLE_SMART, 0x00ff00); //设置空白字符的默认前景色 - //execute(SCI_SETWHITESPACEFORE, true, 0x6ab5ff); + //execute(SCI_SETWHITESPACEFORE, true, 0x6ab5ff); execute(SCI_SETWHITESPACESIZE,3); setCaretLineVisible(true); @@ -1297,7 +1487,7 @@ void ScintillaEditView::showBigTextLineAddr(qint64 fileOffset, qint64 fileEndOff //首行地址存在,从头到尾增加行号 if (startLineExist) { - + for (int i = 0; i < lineNums; ++i) { if (i == (lineNums - 1)) @@ -1978,7 +2168,7 @@ void ScintillaEditView::contextUserDefineMenuEvent(QMenu* menu) menu->addAction(tr("Word Count"), [this]() { showWordNums(); }); - + } menu->show(); } @@ -2167,7 +2357,7 @@ bool ScintillaEditView::doBlockComment(Comment_Mode currCommentMode) continue; } } - else + else { if ((qstrncmp(linebufStr.data(), advCommentStart.data(), advCommentStart_length - 1) == 0) && (qstrncmp(linebufStr.mid(linebufStr.length() - advCommentEnd_length + 1, advCommentEnd_length - 1).data(), advCommentEnd.mid(1, advCommentEnd_length - 1).data(), advCommentEnd_length - 1) == 0)) @@ -2318,7 +2508,7 @@ void ScintillaEditView::showWordNums() QMessageBox::about(this, tr("Word Nums"), tr("Current Doc Word Nums is %1 . \nLine nums is %2 . \nSpace nums is %3, Non-space is %4 .").\ arg(text.size() - wrapNums).arg(lineNum).arg(blank - wrapNums).arg(text.size() - blank)); } - + } bool ScintillaEditView::undoStreamComment(bool tryBlockComment) @@ -2689,7 +2879,7 @@ void ScintillaEditView::mouseDoubleClickEvent(QMouseEvent * e) { emit delayWork(); } - + } void ScintillaEditView::changeCase(const TextCaseType & caseToConvert, QString& strToConvert) const @@ -2710,7 +2900,7 @@ void ScintillaEditView::changeCase(const TextCaseType & caseToConvert, QString& { strToConvert = strToConvert.toLower(); break; - } + } case TITLECASE_FORCE: case TITLECASE_BLEND: { @@ -2874,7 +3064,7 @@ std::pair ScintillaEditView::getSelectionLinesRange(intptr_t sel if ((line1 != line2) && (static_cast(execute(SCI_POSITIONFROMLINE, line2)) == end_pos)) { - // if the end of the selection includes the line-ending, + // if the end of the selection includes the line-ending, // then don't include the following line in the range --line2; } @@ -3307,7 +3497,7 @@ void ScintillaEditView::columnReplace(ColumnModeInfos& cmi, int initial, int inc cmi[i]._selLpos += totalDiff; cmi[i]._selRpos += totalDiff; - + const bool hasVirtualSpc = cmi[i]._nbVirtualAnchorSpc > 0; if (hasVirtualSpc) // if virtual space is present, then insert space @@ -3721,7 +3911,7 @@ void ScintillaEditView::setStyleOptions() } else { - setCaretLineBackgroundColor(QColor(0xe8e8ff)); + setCaretLineBackgroundColor(QColor(0xe8e8ff)); setMatchedBraceForegroundColor(QColor(191, 141, 255)); setMatchedBraceBackgroundColor(QColor(222, 222, 222)); setCaretForegroundColor(QColor(0, 0, 0)); @@ -3754,9 +3944,9 @@ static void getFoldColor(QColor& fgColor, QColor& bgColor, QColor& activeFgColor //这里看起来反了,但是实际代码就是如此 fgColor = StyleSet::s_global_style->fold.bgColor; bgColor = StyleSet::s_global_style->fold.fgColor; - + activeFgColor = StyleSet::s_global_style->fold_active.fgColor; - + } void ScintillaEditView::setGlobalFgColor(int style) @@ -3800,7 +3990,7 @@ void ScintillaEditView::setGlobalFgColor(int style) case CURRENT_LINE_BACKGROUND_COLOR: //不能设置前景色,只能设置背景 break; - + case SELECT_TEXT_COLOR: SendScintilla(SCI_SETSELFORE, true, StyleSet::s_global_style->select_text_color.fgColor); break; @@ -4335,7 +4525,7 @@ void ScintillaEditView::on_viewMarkdown() m_markdownWin->setAttribute(Qt::WA_DeleteOnClose); connect(this, &ScintillaEditView::textChanged, this, &ScintillaEditView::on_updataMarkdown); } - + QString text = this->text(); m_markdownWin->viewMarkdown(text); m_markdownWin->show(); diff --git a/src/scintillaeditview.h b/src/scintillaeditview.h index 71180e493632da34cd598eb90fc38fab4599e525..2c59ea7c57928742dfef78782f50ed19ed8369c6 100755 --- a/src/scintillaeditview.h +++ b/src/scintillaeditview.h @@ -4,10 +4,12 @@ #include #include #include +#include #include #include "common.h" #include "Sorters.h" #include "markdownview.h" +#include "tinyexpr.h" typedef sptr_t(*SCINTILLA_FUNC) (sptr_t ptr, unsigned int, uptr_t, sptr_t); @@ -48,11 +50,11 @@ enum TextCaseType RANDOMCASE }; -enum Comment_Mode -{ - cm_comment = 0, - cm_uncomment, - cm_toggle +enum Comment_Mode +{ + cm_comment = 0, + cm_uncomment, + cm_toggle }; const bool L2R = true; const bool R2L = false; @@ -171,7 +173,7 @@ public: void bookmarkToggle(intptr_t lineno) const; void bookmarkClearAll() const; void bookmarkNext(bool forwardScan); - + void cutMarkedLines(); void copyMarkedLines(); void replaceMarkedline(int ln, QByteArray & str); @@ -204,7 +206,7 @@ public: void setGlobalFgColor(int style); void setGlobalBgColor(int style); void setGlobalFont(int style); - + //获取当前块的开始行号。只在大文件只读模式下有效。其余模式下均返回0 quint32 getBigTextBlockStartLine(); void setBigTextBlockStartLine(quint32 line); @@ -267,7 +269,7 @@ protected: void mouseDoubleClickEvent(QMouseEvent *e) override; void contextUserDefineMenuEvent(QMenu * menu) override; - + public slots: void updateLineNumberWidth(int lineNumberMarginDynamicWidth=0); @@ -292,6 +294,16 @@ private: void replaceSelWith(const char* replaceText); void showWordNums(); + + enum evltype {EVAL_ENTER, EVAL_QUESTION, EVAL_JIT}; + bool tinyexprCalc(evltype evt); + static inline const QSet s_teSymbols = { + '+','-','*','/',':','^','%','!','<','>','(',')', + ',',' ','\t','m','h','s', + '0','1','2','3','4','5','6','7','8','9' + }; + static inline const QSet s_teLookfwdSymbols = {'|','&'}; + private slots: void slot_delayWork(); void slot_scrollYValueChange(int value); @@ -331,6 +343,8 @@ private: QPointer m_markdownWin; + te_parser m_tinyEpr; + public: static int s_tabLens; static bool s_noUseTab; diff --git a/src/tinyexpr-plusplus/Examples.md b/src/tinyexpr-plusplus/Examples.md new file mode 100644 index 0000000000000000000000000000000000000000..f88a338028377d384456c3ae8a9f2a34f3fd16b2 --- /dev/null +++ b/src/tinyexpr-plusplus/Examples.md @@ -0,0 +1,213 @@ +# Examples + +The following are examples demonstrating how to use TinyExpr++. + +## Example 1 + +```cpp +include "tinyexpr.h" +#include + +int main(int argc, char *argv[]) + { + te_parser tep; + const char *c = "sqrt(5^2+7^2+11^2+(8-2)^2)"; + double r = tep.evaluate(c); + std::cout << "The expression:\n\t" << + c << "\nevaluates to:\n\t" << r << "\n"; + return EXIT_SUCCESS; + } +``` + +## Example 2: Binding Custom Variables + +```cpp +#include "tinyexpr.h" +#include +#include + +int main(int argc, char* argv[]) + { + if (argc < 2) + { + std::cout << "Usage: example \"expression\"\n"; + return EXIT_SUCCESS; + } + + const char* expression = argv[1]; + std::cout << "Evaluating:\n\t" << expression << "\n"; + + /* This shows an example where the variables + x and y are bound at eval-time. */ + double x{ 0 }, y{ 0 }; + // Store variable names and pointers. + te_parser tep; + tep.set_variables_and_functions({ {"x", &x}, {"y", &y} }); + + /* This will compile the expression and check for errors. */ + if (tep.compile(expression)) + { + /* The variables can be changed here, and eval can be called as many + * times as you like. This is fairly efficient because the parsing has + * already been done. */ + x = 3; y = 4; + const double r = tep.evaluate(); + std::cout << "Result:\n\t" << r << "\n"; + } + else + { + /* Show the user where the error is at. */ + std::cout << "\t " << std::setfill(' ') << + std::setw(tep.get_last_error_position()) << '^' << + "\tError near here\n"; + } + + return EXIT_SUCCESS; + } +``` + +## Example 3: Calling a Free Function + +```cpp +#include "tinyexpr.h" +#include +#include + +/* An example of calling a free function. */ +double my_sum(double a, double b) + { + std::cout << "Called C function with " << + a << " and " << b << ".\n"; + return a + b; + } + +int main(int argc, char *argv[]) + { + const char *expression = "mysum(5, 6)"; + std::cout << "Evaluating:\n\t" << expression << "\n"; + + te_parser tep; + tep.set_variables_and_functions({{"mysum", my_sum}}); + + if (tep.compile(expression)) + { + const double r = tep.evaluate(); + std::cout << "Result:\n\t" << r << "\n"; + } + else + { + /* Show the user where the error is at. */ + std::cout << "\t " << std::setfill(' ') << + std::setw(tep.get_last_error_position()) << '^' << + "\tError near here\n"; + } + + return EXIT_SUCCESS; + } +``` + +## Example 4: Non-US Formatted Formulas + +```cpp +#include "tinyexpr.h" +#include +#include +#include +#include + +int main(int argc, char *argv[]) + { + /* Set locale to German. + This string is platform dependent. The following works on Windows, + consult your platform's documentation for more details.*/ + setlocale(LC_ALL, "de-DE"); + std::locale::global(std::locale("de-DE")); + + /* After setting your locale to German, functions like strtod() will fail + with values like "3.14" because it expects "3,14" instead. + To fix this, we will tell the parser to use "," as the decimal separator + and ";" as list argument separator.*/ + + const char *expression = "pow(2,2; 2)"; // instead of "pow(2.2, 2)" + std::cout << "Evaluating:\n\t" << expression << "\n"; + + te_parser tep; + tep.set_decimal_separator(','); + tep.set_list_separator(';'); + + if (tep.compile(expression)) + { + const double r = tep.evaluate(); + std::cout << "Result:\n\t" << r << "\n"; + } + else + { + /* Show the user where the error is at. */ + std::cout << "\t " << std::setfill(' ') << + std::setw(tep.get_last_error_position()) << '^' << + "\tError near here\n"; + } + + return EXIT_SUCCESS; + } +``` + +## Example 5: Binding to Custom Classes + +A class derived from `te_expr` can be bound to custom functions. This enables you to +have full access to an object (via these functions) when parsing an expression. + +The following demonstrates creating a `te_expr`-derived class which contains an array of values: + +```cpp +class te_expr_array : public te_expr + { +public: + explicit te_expr_array(const variable_flags type) noexcept : + te_expr(type) {} + std::array m_data = { 5, 6, 7, 8, 9 }; + }; +``` + +Next, create two functions that can accept this object and perform +actions on it. (Note that proper error handling is not shown for brevity.): + +```cpp +// Returns the value of a cell from the object's data. +double cell(const te_expr* context, double a) + { + auto* c = dynamic_cast(context); + return static_cast(c->m_data[static_cast(a)]); + } + +// Returns the max value of the object's data. +double cell_max(const te_expr* context) + { + auto* c = dynamic_cast(context); + return static_cast( + *std::max_element(c->m_data.cbegin(), c->m_data.cend())); + } +``` + +Finally, create an instance of the class and connect the custom functions to it, +while also adding them to the parser: + +```cpp +te_expr_array teArray{ TE_DEFAULT }; + +te_parser tep; +tep.set_variables_and_functions( + { + {"cell", cell, TE_DEFAULT, &teArray}, + {"cellmax", cell_max, TE_DEFAULT, &teArray} + }); + +// change the object's data and evaluate their summation +// (will be 30) +teArray.m_data = { 6, 7, 8, 5, 4 }; +auto result = tep.evaluate("SUM(CELL 0, CELL 1, CELL 2, CELL 3, CELL 4)"); + +// call the other function, getting the object's max value +// (will be 8) +res = tep.evaluate("CellMax()"); +``` \ No newline at end of file diff --git a/src/tinyexpr-plusplus/LICENSE b/src/tinyexpr-plusplus/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..fcafb5c1f088a4dc6bf1a4a1e2032669d1ccb343 --- /dev/null +++ b/src/tinyexpr-plusplus/LICENSE @@ -0,0 +1,19 @@ +zlib License + +Copyright (C) 2015, 2016 Lewis Van Winkle + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/src/tinyexpr-plusplus/README.md b/src/tinyexpr-plusplus/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bb7147a4c3fc030471ac7216668f5b6457fee1e3 --- /dev/null +++ b/src/tinyexpr-plusplus/README.md @@ -0,0 +1,449 @@ +TinyExpr + +# TinyExpr++ + +Manual + +*TinyExpr++* is the C++ version of the [*TinyExpr*](https://github.com/codeplea/tinyexpr) library, which is a small +recursive-descent parser and evaluation engine for math expressions. + +In addition to math operators and precedence, *TinyExpr++* also supports +the standard C math functions and runtime binding of variables and user-defined functions. + +Please refer to the [TinyExpr++ Reference Manual](docs/TinyExpr++ReferenceManual.pdf) +for a full list of features. + +| Platforms | Result | +| ------------- | ------------- | +| Linux | [![unit-tests](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/unit-tests.yml) | +| macOS | [![macOS Build & Unit Tests](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/macos-unit-tests.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/macos-unit-tests.yml) | +| Windows | [![Windows Build & Unit Tests](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/msw-unit-tests.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/msw-unit-tests.yml) | + + +| Code Analyses | Result | +| ------------- | ------------- | +| cppcheck | [![cppcheck](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/cppcheck.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/cppcheck.yml) | +| MS PREfast | [![Microsoft C++ Code Analysis](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/msvc.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/msvc.yml) | +| CodeQL | [![CodeQL](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/codeql.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/codeql.yml) | +| i18n-check | [![i18n-check](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/i18n-check.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/i18n-check.yml) | + + +| Documentation Checks | Result | +| ------------- | ------------- | +| Doxygen | [![doxygen](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/doxygen.yml/badge.svg)](https://github.com/Blake-Madden/tinyexpr-plusplus/actions/workflows/doxygen.yml) | +| Spellcheck | [![Spell Check](https://github.com/Blake-Madden/i18n-check/actions/workflows/spell-check.yml/badge.svg)](https://github.com/Blake-Madden/i18n-check/actions/workflows/spell-check.yml) | + +## Compatibility Advisory + +Note: for current users of *TinyExpr++*, please see the [compatibility advisory](CompatibilityAdvisory.md) for recent changes. + +## Embedded Programming + +For notes on embedded programming, please refer to the [embedded programming](Embedded.md) overview. + +## Features + +- **C++17 with no dependencies**. +- Single source file and header file. +- Simple and fast. +- Implements standard operator precedence. +- Implements logical and comparison operators. +- Exposes standard C math functions (`sin`, `sqrt`, `ln`, etc.), as well as some *Excel*-like functions (e.g., `AVERAGE()` and `IF()`). +- Can add custom functions and variables easily. +- Can bind constants at eval-time. +- Supports variadic functions (taking between 1-7 arguments). +- Case insensitive. +- Supports non-US formulas (e.g., `POW(2,2; 2)` instead of `POW(2.2, 2)`). +- Supports C and C++ style comments within math expressions. +- Released under the zlib license - free for nearly any use. +- Easy to use and integrate with your code. +- Thread-safe; parser is in a self-contained object. + +## Changes from TinyExpr + +Please refer [here](TinyExprChanges.md) for a list of changes from the original *TinyExpr* C library. + +## Building + +*TinyExpr++* is self-contained in two files: "tinyexpr.cpp and "tinyexpr.h". To use +*TinyExpr++*, simply add those two files to your project. + +The documentation can be built using the following: + +``` +doxygen docs/Doxyfile +``` + +## Short Example + +Here is a minimal example to evaluate an expression at runtime. + +```cpp +#include "tinyexpr.h" +#include +#include +#include + +te_parser tep; + +const double r = tep.evaluate("sqrt(5^2+7^2+11^2+(8-2)^2)"); +std::cout << std::setprecision(8) << "The expression:\n\t" << + tep.get_expression() << "\nevaluates to:\n\t" << + r << "\n"; +// prints 15.198684 +``` + +## Usage + +*TinyExpr++*'s `te_parser` class defines these functions: + +```cpp +double evaluate(const std::string_view expression); +double get_result(); +bool success(); +int64_t get_last_error_position(); +std::string get_last_error_message(); +set_variables_and_functions(const std::set& vars); +std::set& get_variables_and_functions(); +add_variable_or_function(const te_variable& var); +set_unknown_symbol_resolver(te_usr_variant_type usr); +get_decimal_separator(); +set_decimal_separator(); +get_list_separator(); +set_list_separator(); +``` + +`evaluate()` takes an expression and immediately returns the result of it. If there +is a parse error, then it returns NaN (which can be verified by using `std::isnan()`). + +`get_result()` can be called anytime afterwards to retrieve the result from `evaluate()`. + +`success()` can be called to see if the previous call `evaluate()` succeeded or not. + +If the parse failed, calling `get_last_error_position()` will return the 0-based index of where in the +expression the parse failed. For some errors, `get_last_error_message()` will return a more detailed message. + +`set_variables_and_functions()`, `get_variables_and_functions()`, and `add_variable_or_function()` are used +to add custom variables and functions to the parser. + +`set_unknown_symbol_resolver()` is used to provide a custom function to resolve unknown symbols in an expression. + +`get_decimal_separator()`/`set_decimal_separator()` and +`get_list_separator()`/`set_list_separator()` can be used to parse non-US formatted formulas. + +Example usage: + +```cpp +te_parser tep; + +// Returns 10, error position is set to te_parser::npos (i.e., no error). +double result = tep.evaluate("(5+5)"); +// Returns NaN, error position is set to 3. +double result2 = tep.evaluate("(5+5"); +``` + +Give `set_variables_and_functions()` a list of constants, bound variables, and function pointers/lambdas. + +`evaluate()` will then evaluate expressions using these variables and functions. + +**example usage:** + +```cpp +#include "tinyexpr.h" +#include + +double x{ 0 }, y{ 0 }; +// Store variable names and pointers. +te_parser tep; +tep.set_variables_and_functions({{"x", &x}, {"y", &y}}); + +// Compile the expression with variables. +auto result = tep.evaluate("sqrt(x^2+y^2)"); + +if (tep.success()) + { + x = 3; y = 4; + // Will use the previously used expression, returns 5. + const double h1 = tep.evaluate(); + + x = 5; y = 12; + // Returns 13. + const double h2 = tep.evaluate(); + } +else + { + std::cout << "Parse error at " << + std::to_string(tep.get_last_error_position()) << "\n"; + } +``` + +## Longer Example + +Here is a complete example that will evaluate an expression passed in from the command +line. It also does error checking and binds the variables `x` and `y` to `3` and `4`, respectively. + +```cpp +#include "tinyexpr.h" +#include +#include + +int main(int argc, char *argv[]) + { + if (argc < 2) + { + std::cout << "Usage: example \"expression\"\n"; + return EXIT_FAILURE; + } + + const char *expression = argv[1]; + std::cout << "Evaluating:\n\t" << expression << "\n"; + + /* This shows an example where the variables + x and y are bound at eval-time. */ + double x{ 0 }, y{ 0 }; + // Store variable names and pointers. + te_parser tep; + tep.set_variables_and_functions({{"x", &x}, {"y", &y}}); + + /* This will compile the expression and check for errors. */ + auto result = tep.evaluate(expression); + + if (tep.success()) + { + /* The variables can be changed here, and eval can be called as many + times as you like. This is fairly efficient because the parsing has + already been done. */ + x = 3; y = 4; + const double r = tep.evaluate(); + std::cout << "Result:\n\t" << r << "\n"; + } + else + { + /* Show the user where the error is at. */ + std::cout << "\t " << std::setfill(' ') << + std::setw(tep.get_last_error_position()) << '^' << + "\tError near here\n"; + } + + return EXIT_SUCCESS; + } +``` + +This produces the output: + + $ "sqrt(x^2+y2)" + Evaluating: + sqrt(x^2+y2) + ^ + Error near here + + $ "sqrt(x^2+y^2)" + Evaluating: + sqrt(x^2+y^2) + Result: + 5.000000 + +## Binding to Custom Functions + +*TinyExpr++* can also call custom functions. Here is a short example: + +```cpp +double my_sum(double a, double b) + { + /* Example function that adds two numbers together. */ + return a + b; + } + +te_parser tep; +tep.set_variables_and_functions( +{ + { "mysum", my_sum } // function pointer +}); + +const double r = tep.evaluate("mysum(5, 6)"); +// will be 11 +``` + +Here is an example of using a lambda: + +```cpp +te_parser tep; +tep.set_variables_and_functions({ + { "mysum", + [](double a, double b) noexcept + { return a + b; } } + }); + +const double r = tep.evaluate("mysum(5, 6)"); +// will be 11 +``` + +## Binding to Custom Classes + +A class derived from `te_expr` can be bound to custom functions. This enables you to +have full access to an object (via these functions) when parsing an expression. + +The following demonstrates creating a `te_expr`-derived class which contains an array of values: + +```cpp +class te_expr_array : public te_expr + { +public: + explicit te_expr_array(const te_variable_flags type) noexcept : + te_expr(type) {} + std::array m_data = { 5, 6, 7, 8, 9 }; + }; +``` + +Next, create two functions that can accept this object and perform +actions on it. (Note that proper error handling is not included for brevity.): + +```cpp +// Returns the value of a cell from the object's data. +double cell(const te_expr* context, double a) + { + auto* c = dynamic_cast(context); + return static_cast(c->m_data[static_cast(a)]); + } + +// Returns the max value of the object's data. +double cell_max(const te_expr* context) + { + auto* c = dynamic_cast(context); + return static_cast( + *std::max_element(c->m_data.cbegin(), c->m_data.cend())); + } +``` + +Finally, create an instance of the class and connect the custom functions to it, +while also adding them to the parser: + +```cpp +te_expr_array teArray{ TE_DEFAULT }; + +te_parser tep; +tep.set_variables_and_functions( + { + {"cell", cell, TE_DEFAULT, &teArray}, + {"cellmax", cell_max, TE_DEFAULT, &teArray} + }); + +// change the object's data and evaluate their summation +// (will be 30) +teArray.m_data = { 6, 7, 8, 5, 4 }; +auto result = tep.evaluate("SUM(CELL 0, CELL 1, CELL 2, CELL 3, CELL 4)"); + +// call the other function, getting the object's max value +// (will be 8) +res = tep.evaluate("CellMax()"); +``` + +## Non-US Formatted Formulas + +*TinyExpr++* supports other locales and non-US formatted formulas. Here is an example: + +```cpp +#include "tinyexpr.h" +#include +#include +#include +#include + +int main(int argc, char *argv[]) + { + /* Set locale to German. + This string is platform dependent. The following works on Windows, + consult your platform's documentation for more details.*/ + setlocale(LC_ALL, "de-DE"); + std::locale::global(std::locale("de-DE")); + + /* After setting your locale to German, functions like strtod() will fail + with values like "3.14" because it expects "3,14" instead. + To fix this, we will tell the parser to use "," as the decimal separator + and ";" as list argument separator.*/ + + const char *expression = "pow(2,2; 2)"; // instead of "pow(2.2, 2)" + std::cout << "Evaluating:\n\t" << expression << "\n"; + + te_parser tep; + tep.set_decimal_separator(','); + tep.set_list_separator(';'); + + /* This will compile the expression and check for errors. */ + auto r = tep.evaluate(expression); + + if (tep.success()) + { + const double r = tep.evaluate(expression); + std::cout << "Result:\n\t" << r << "\n"; + } + else + { + /* Show the user where the error is at. */ + std::cout << "\t " << std::setfill(' ') << + std::setw(tep.get_last_error_position()) << '^' << + "\tError near here\n"; + } + + return EXIT_SUCCESS; + } +``` + +This produces the output: + + $ Evaluating: + pow(2,2; 2) + Result: + 4,840000 + +Refer to [Examples](Examples.md) for more examples. + +## How it Works + +`te_parser::evaluate()` uses a simple recursive descent parser to compile your +expression into a syntax tree. For example, the expression `"sin x + 1/4"` +parses as: + +![example syntax tree](docs/e1.png) + +`te_parser::evaluate()` also automatically prunes constant branches. In this example, +the compiled expression returned by `te_compile()` would become: + +![example syntax tree](docs/e2.png) + +## Grammar + +*TinyExpr++* parses the following grammar (from lowest-to-highest operator precedence): + + = {(",", ";" [dependent on locale]) } + = {("&" | "|") } + = {("<>" | "!=" | "=" | "<") | "<=") | ">" | ">=") } + = {("<<" | ">>") } + = {("+" | "-") } + = {("*" | "/" | "%") } + = {("^" | "**") } + = {("-" | "+")} + = + | + | {"(" ")"} + | + | "(" {"," } ")" + | "(" ")" + +In addition, whitespace between tokens is ignored. + +Valid variable names consist of a letter or underscore followed by any combination +of: letters `a`–`z` or `A`–`Z`, digits `0`–`9`, periods, and +underscores. Constants can be integers, decimal numbers, or in scientific +notation (e.g., `1e3` for `1000`). A leading zero is not required (e.g., `.5` +for `0.5`). + +## Supported Functions + +*TinyExpr++* supports addition (`+`), subtraction/negation (`-`), multiplication (`*`), +division (`/`), exponentiation (`^`), modulus (`%`), and left/right shift (`<<`, `>>`) +with the normal operator precedence (the one exception being that exponentiation is evaluated +left-to-right, but this can be changed - see below). + +Please refer to the [TinyExpr++ Reference Manual](docs/TinyExpr++ReferenceManual.pdf) +for a full list of available functions. \ No newline at end of file diff --git a/src/tinyexpr-plusplus/TinyExpr++ReferenceManual.pdf b/src/tinyexpr-plusplus/TinyExpr++ReferenceManual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e0f81f0743e3348cbeef82f42f9cea75178a3a1f Binary files /dev/null and b/src/tinyexpr-plusplus/TinyExpr++ReferenceManual.pdf differ diff --git a/src/tinyexpr-plusplus/tinyexpr.cpp b/src/tinyexpr-plusplus/tinyexpr.cpp new file mode 100644 index 0000000000000000000000000000000000000000..11d569e802fec7157c80576a21d6631435f4a30a --- /dev/null +++ b/src/tinyexpr-plusplus/tinyexpr.cpp @@ -0,0 +1,1411 @@ +// SPDX-License-Identifier: Zlib +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015-2020 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* + * TINYEXPR++ - Tiny recursive descent parser and evaluation engine in C++ + * + * Copyright (c) 2020-2023 Blake Madden + * + * C++ version of the TinyExpr library. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "tinyexpr.h" + +// builtin functions +[[nodiscard]] +constexpr static te_type _te_equal(te_type a, te_type b) noexcept + { return static_cast((a == b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_not_equal(te_type a, te_type b) noexcept + { return static_cast((a != b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_less_than(te_type a, te_type b) noexcept + { return static_cast((a < b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_less_than_equal_to(te_type a, te_type b) noexcept + { return static_cast((a <= b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_greater_than(te_type a, te_type b) noexcept + { return static_cast((a > b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_greater_than_equal_to(te_type a, te_type b) noexcept + { return static_cast((a >= b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_and(te_type a, te_type b) noexcept + { return static_cast((a && b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_or(te_type a, te_type b) noexcept + { return static_cast((a || b) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_not(te_type a) noexcept + { return !a; } +[[nodiscard]] +constexpr static te_type _te_pi() noexcept + { return static_cast(3.14159265358979323846); } +[[nodiscard]] +constexpr static te_type _te_e() noexcept + { return static_cast(2.71828182845904523536); } +[[nodiscard]] +static te_type _te_fac(te_type a) noexcept {/* simplest version of factorial */ + if (a < 0.0 || std::isnan(a)) + { return te_parser::te_nan; } + if (a > (std::numeric_limits::max)()) + { return std::numeric_limits::infinity(); } + const auto ua = static_cast(a); + unsigned long int result{ 1 }, i{ 1 }; + for (i = 1; i <= ua; i++) + { + if (i > (std::numeric_limits::max)() / result) + return std::numeric_limits::infinity(); + result *= i; + } + return static_cast(result); +} + +[[nodiscard]] +static te_type _te_absolute_value(te_type n) + { return std::fabs(static_cast(n)); } + +[[nodiscard]] +static te_type _te_log(te_type x) + { return std::log(static_cast(x)); } + +[[nodiscard]] +static te_type _te_log10(te_type x) + { return std::log10(static_cast(x)); } + +[[nodiscard]] +static te_type _te_pow(te_type x, te_type y) + { return std::pow(static_cast(x), static_cast(y)); } + +[[nodiscard]] +static te_type _te_tan(te_type x) + { return std::tan(static_cast(x)); } + +[[nodiscard]] +static te_type _te_tanh(te_type x) + { return std::tanh(static_cast(x)); } + +[[nodiscard]] +static te_type _te_trunc(te_type x) + { return std::trunc(static_cast(x)); } + +[[nodiscard]] +static te_type _te_sin(te_type x) + { return std::sin(static_cast(x)); } + +[[nodiscard]] +static te_type _te_sinh(te_type x) + { return std::sinh(static_cast(x)); } + +[[nodiscard]] +static te_type _te_sqrt(te_type x) + { + if (x < 0) + { + throw std::runtime_error("Negative value passed to SQRT."); + } + return std::sqrt(static_cast(x)); + } + +[[nodiscard]] +static te_type _te_floor(te_type x) + { return std::floor(static_cast(x)); } + +[[nodiscard]] +static te_type _te_ceil(te_type x) + { return std::ceil(static_cast(x)); } + +[[nodiscard]] +static te_type _te_exp(te_type x) + { return std::exp(static_cast(x)); } + +[[nodiscard]] +static te_type _te_cos(te_type x) + { return std::cos(static_cast(x)); } + +[[nodiscard]] +static te_type _te_cosh(te_type x) + { return std::cosh(static_cast(x)); } + +[[nodiscard]] +static te_type _te_acos(te_type x) + { return std::acos(static_cast(x)); } + +[[nodiscard]] +static te_type _te_asin(te_type x) + { + if (std::isfinite(x) && + (x < -1.0 || x > 1.0)) + { + throw std::runtime_error( + "Argument passed to ASIN must be between -1 and 1."); + } + return std::asin(static_cast(x)); + } + +[[nodiscard]] +static te_type _te_atan(te_type x) + { return std::atan(static_cast(x)); } + +[[nodiscard]] +static te_type _te_atan2(te_type y, te_type x) + { return std::atan2(static_cast(y), (static_cast(x))); } + +[[nodiscard]] +static te_type _te_tgamma(te_type y) + { return std::tgamma(y); } + +[[nodiscard]] +static te_type _te_random() + { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution distr(0, 1); + return distr(gen); + } +[[nodiscard]] +constexpr static te_type _te_divide(te_type a, te_type b) + { + if (b == 0) + { throw std::runtime_error("Division by zero."); } + return a / b; + } +[[nodiscard]] +static te_type _te_modulus(te_type a, te_type b) + { + if (b == 0) + { throw std::runtime_error("Modulus by zero."); } + return std::fmod(a,b); + } +[[nodiscard]] +static te_type _te_sum(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) + { + return (std::isnan(v1) ? 0 : v1) + + (std::isnan(v2) ? 0 : v2) + + (std::isnan(v3) ? 0 : v3) + + (std::isnan(v4) ? 0 : v4) + + (std::isnan(v5) ? 0 : v5) + + (std::isnan(v6) ? 0 : v6) + + (std::isnan(v7) ? 0 : v7); + } +[[nodiscard]] +static te_type _te_average(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) + { + const auto validN = (std::isnan(v1) ? 0 : 1) + + (std::isnan(v2) ? 0 : 1) + + (std::isnan(v3) ? 0 : 1) + + (std::isnan(v4) ? 0 : 1) + + (std::isnan(v5) ? 0 : 1) + + (std::isnan(v6) ? 0 : 1) + + (std::isnan(v7) ? 0 : 1); + const auto total = _te_sum(v1, v2, v3, v4, v5, v6, v7); + return _te_divide(total, static_cast(validN)); + } + +/// @warning This version of round emulates Excel behavior of supporting +/// negative decimal places (e.g., ROUND(21.5, -1) = 20). Be aware +/// of that if using this function outside of TinyExpr++. +[[nodiscard]] +static te_type _te_round(te_type val, te_type decimal_places) + { + const bool useNegativeRound{ decimal_places < 0 }; + const size_t adjustedDecimalPlaces{ + std::isnan(decimal_places) ? + 0 : static_cast(std::abs(decimal_places)) }; + + const te_type decimalPostition = static_cast(std::pow(10, adjustedDecimalPlaces)); + if (!std::isfinite(decimalPostition)) + { return te_parser::te_nan; } + + if (!useNegativeRound) + { + if (val < 0) + { + return (decimalPostition == 0) ? std::ceil(val - static_cast(0.5)) : + std::ceil(static_cast(val * decimalPostition) - static_cast(0.5)) / + decimalPostition; + } + else + { + return (decimalPostition == 0) ? std::floor(val + static_cast(0.5)) : + std::floor(static_cast(val * decimalPostition) + static_cast(0.5)) / + decimalPostition; + } + } + else + { + // ROUND(21.5, -1) = 20 + if (val < 0) + { + return std::ceil(static_cast(val / decimalPostition) - static_cast(0.5)) * + decimalPostition; + } + else + { + return std::floor(static_cast(val / decimalPostition) + static_cast(0.5)) * + decimalPostition; + } + } + } + +// Combinations (without repetition) +[[nodiscard]] +static te_type _te_ncr(te_type n, te_type r) noexcept + { + if (n < 0.0 || r < 0.0 || n < r || std::isnan(n) || std::isnan(r)) + { return te_parser::te_nan; } + if (n > ((std::numeric_limits::max)()) || r > + (std::numeric_limits::max)()) + { return std::numeric_limits::infinity(); } + const unsigned long int un{ static_cast(n) }; + unsigned long int ur{ static_cast(r) }; + unsigned long int result{ 1 }; + if (ur > un / 2) ur = un - ur; + for (decltype(ur) i = 1; i <= ur; i++) + { + if (result > ((std::numeric_limits::max)()) / (un - ur + i)) + return std::numeric_limits::infinity(); + result *= un - ur + i; + result /= i; + } + return static_cast(result); + } +// Permutations (without repetition) +[[nodiscard]] +static te_type _te_npr(te_type n, te_type r) noexcept + { return _te_ncr(n, r) * _te_fac(r); } + +[[nodiscard]] +constexpr static te_type _te_add(te_type a, te_type b) noexcept + { return a + b; } +[[nodiscard]] +constexpr static te_type _te_sub(te_type a, te_type b) noexcept + { return a - b; } +[[nodiscard]] +constexpr static te_type _te_mul(te_type a, te_type b) noexcept + { return a * b; } + +// Shift operators +//-------------------------------------------------- +[[nodiscard]] +static te_type _te_left_shift(te_type a, te_type b) + { + if (std::floor(a) != a) + { + throw std::runtime_error( + "Left side of left shift (<<) operation must be an integer."); + } + else if (std::floor(b) != b) + { + throw std::runtime_error( + "Additive expression of left shift (<<) operation must be an integer."); + } + else if (a < 0) + { + throw std::runtime_error( + "Left side of left shift (<<) operation cannot be negative."); + } + // bitness is limited to 64-bit, so ensure shift doesn't go beyond that + // and cause undefined behavior + else if (b < 0 || b >= 64) + { + throw std::runtime_error( + "Additive expression of left shift (<<) operation must be between 0-63."); + } + const auto multipler = (static_cast(1) << static_cast(b)); + const auto maxBaseNumber = (std::numeric_limits::max() / multipler); + if (static_cast(a) > maxBaseNumber) + { + throw std::runtime_error( + "Overflow in left shift (<<) operation; base number is too large."); + } + return static_cast(static_cast(a) << static_cast(b)); + } + +//-------------------------------------------------- +[[nodiscard]] +static te_type _te_right_shift(te_type a, te_type b) + { + if (std::floor(a) != a) + { + throw std::runtime_error( + "Left side of right shift (>>) operation must be an integer."); + } + else if (std::floor(b) != b) + { + throw std::runtime_error( + "Additive expression of right shift (>>)operation must be an integer."); + } + else if (a < 0) + { + throw std::runtime_error( + "Left side of right shift (<<) operation cannot be negative."); + } + else if (b < 0 || b >= 64) + { + throw std::runtime_error( + "Additive expression of right shift (>>) operation must be between 0-63."); + } + return static_cast(static_cast(a) >> static_cast(b)); + } + +/// @warning This emulates Excel, where a negative shift amount acts as a right shift.\n +/// Be aware of this if using this function outside of TinyExpr++. +//-------------------------------------------------- +[[nodiscard]] +static te_type _te_left_shift_or_right(te_type a, te_type b) + { + return (b >= 0) ? + _te_left_shift(a, b) : _te_right_shift(a, std::abs(b)); + } + +/// @warning This emulates Excel, where a negative shift amount acts as a right shift.\n +/// Be aware of this if using this function outside of TinyExpr++. +//-------------------------------------------------- +[[nodiscard]] +static te_type _te_right_shift_or_left(te_type a, te_type b) + { + return (b >= 0) ? + _te_right_shift(a, b) : _te_left_shift(a, std::abs(b)); + } + +[[nodiscard]] +constexpr static te_type _te_sqr(te_type a) noexcept + { return a*a; } +[[nodiscard]] +static te_type _te_max_maybe_nan(te_type v1, te_type v2_maybe_nan) noexcept + { return (std::max)(v1, std::isnan(v2_maybe_nan) ? v1 : v2_maybe_nan); } +[[nodiscard]] +static te_type _te_max(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) noexcept + { + // assumes that at least v1 is a number, rest can be NaN + auto maxVal = _te_max_maybe_nan(v1, v2); + maxVal = _te_max_maybe_nan(maxVal, v3); + maxVal = _te_max_maybe_nan(maxVal, v4); + maxVal = _te_max_maybe_nan(maxVal, v5); + maxVal = _te_max_maybe_nan(maxVal, v6); + return _te_max_maybe_nan(maxVal, v7); + } +[[nodiscard]] +static te_type _te_min_maybe_nan(te_type v1, te_type v2_maybe_nan) noexcept + { return (std::min)(v1, std::isnan(v2_maybe_nan) ? v1 : v2_maybe_nan); } +[[nodiscard]] +static te_type _te_min(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) noexcept + { + // assumes that at least v1 is legit, rest can be NaN + auto minVal = _te_min_maybe_nan(v1, v2); + minVal = _te_min_maybe_nan(minVal, v3); + minVal = _te_min_maybe_nan(minVal, v4); + minVal = _te_min_maybe_nan(minVal, v5); + minVal = _te_min_maybe_nan(minVal, v6); + return _te_min_maybe_nan(minVal, v7); + } +[[nodiscard]] +static te_type _te_and_maybe_nan(te_type v1, te_type v2_maybe_nan) noexcept + { return std::isnan(v2_maybe_nan) ? v1 : (v1 && v2_maybe_nan); } +[[nodiscard]] +static te_type _te_and_variadic(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) noexcept + { + // assumes that at least v1 is legit, rest can be NaN + auto andVal = _te_and_maybe_nan(v1, v2); + andVal = _te_and_maybe_nan(andVal, v3); + andVal = _te_and_maybe_nan(andVal, v4); + andVal = _te_and_maybe_nan(andVal, v5); + andVal = _te_and_maybe_nan(andVal, v6); + return _te_and_maybe_nan(andVal, v7); + } +[[nodiscard]] +static te_type _te_or_maybe_nan(te_type v1, te_type v2_maybe_nan) noexcept + { return std::isnan(v2_maybe_nan) ? v1 : (v1 || v2_maybe_nan); } +[[nodiscard]] +static te_type _te_or_variadic(te_type v1, te_type v2, te_type v3, te_type v4, + te_type v5, te_type v6, te_type v7) noexcept + { + // assumes that at least v1 is legit, rest can be NaN + auto orVal = _te_or_maybe_nan(v1, v2); + orVal = _te_or_maybe_nan(orVal, v3); + orVal = _te_or_maybe_nan(orVal, v4); + orVal = _te_or_maybe_nan(orVal, v5); + orVal = _te_or_maybe_nan(orVal, v6); + return _te_or_maybe_nan(orVal, v7); + } +[[nodiscard]] +constexpr static te_type _te_if(te_type a, te_type b, te_type c) noexcept + { return (a != 0.0) ? b : c; } +[[nodiscard]] +constexpr static te_type _te_ifs(te_type if1, te_type if1True, + te_type if2, te_type if2True, + te_type if3, te_type if3True) noexcept + { + return (!std::isnan(if1) && if1 != 0.0) ? if1True : + (!std::isnan(if2) && if2 != 0.0) ? if2True : + (!std::isnan(if3) && if3 != 0.0) ? if3True : + te_parser::te_nan; + } +[[nodiscard]] +constexpr static te_type _te_false_value() noexcept + { return 0; } +[[nodiscard]] +constexpr static te_type _te_true_value() noexcept + { return 1; } +[[nodiscard]] +constexpr static te_type _te_nan_value() noexcept + { return te_parser::te_nan; } +// cotangent +[[nodiscard]] +static te_type _te_cot(te_type a) noexcept + { + if (a == 0.0) + { return te_parser::te_nan; } + return 1 / static_cast(std::tan(a)); + } +[[nodiscard]] +constexpr static te_type _te_sign(te_type a) noexcept + { return static_cast((a < 0.0) ? -1 : (a > 0.0) ? 1 : 0); } +[[nodiscard]] +constexpr static te_type _te_negate(te_type a) noexcept + { return -a; } +[[nodiscard]] +constexpr static te_type _comma([[maybe_unused]] te_type a, te_type b) noexcept + { return b; } + +//-------------------------------------------------- +void te_parser::te_free_parameters(te_expr *n) + { + if (!n) return; + if (is_closure(n->m_value)) + { + // last param is the context object, we don't manage that here + for (auto param = n->m_parameters.begin(); + param != n->m_parameters.end() - 1; + ++param) + { + te_free(*param); + *param = nullptr; + } + } + else if (is_function(n->m_value)) + { + for (auto param : n->m_parameters) + { + te_free(param); + param = nullptr; + } + } + } + +//-------------------------------------------------- +const std::set te_parser::m_operators = { + { "+" }, + { "-" }, + { "*" }, + { "/" }, + { "^" }, + { "%" }, + { "<<" }, + { ">>" }, + { "(" }, + { ")" }, + { "=" }, + { "==" }, + { "<>" }, + { "!=" }, + { "<" }, + { "<=" }, + { ">" }, + { ">=" }, + { "&" }, + { "&&" }, + { "|" }, + { "||" }, + { "^" }, + { "**" }, + { "//" }, + { "/*" }, + { "*/" }, + // not currently used, but might be in the future + { "\"" }, + { "'" }, + { "[" }, + { "]" }, + { "!" }, + // list and decimal separators + { "," }, + { ";" }, + { "." } +}; + +//-------------------------------------------------- +const std::set te_parser::m_functions = { + {"abs", static_cast(_te_absolute_value), TE_PURE}, + {"acos", static_cast(_te_acos), TE_PURE}, + // variadic, accepts 1-7 arguments + {"and", static_cast(_te_and_variadic), static_cast(TE_PURE|TE_VARIADIC)}, + {"asin", static_cast(_te_asin), TE_PURE}, + {"atan", static_cast(_te_atan), TE_PURE}, + {"atan2", static_cast(_te_atan2), TE_PURE}, + {"average", static_cast(_te_average), static_cast(TE_PURE|TE_VARIADIC)}, + {"bitlshift", static_cast(_te_left_shift_or_right), TE_PURE}, + {"bitrshift", static_cast(_te_right_shift_or_left), TE_PURE}, + {"ceil", static_cast(_te_ceil), TE_PURE}, + {"clamp", static_cast( + [](const te_type num, const te_type start, const te_type end) + { + return (start <= end) ? + std::clamp(num, start, end) : + std::clamp(num, end, start); + }), + TE_PURE}, + {"combin", static_cast(_te_ncr), TE_PURE}, + {"cos", static_cast(_te_cos), TE_PURE}, + {"cosh", static_cast(_te_cosh), TE_PURE}, + {"cot", static_cast(_te_cot), TE_PURE}, + {"e", static_cast(_te_e), TE_PURE}, + {"exp", static_cast(_te_exp), TE_PURE}, + {"fac", static_cast(_te_fac), TE_PURE}, + {"fact", static_cast(_te_fac), TE_PURE}, + {"false", static_cast(_te_false_value), TE_PURE}, + {"floor", static_cast(_te_floor), TE_PURE}, + {"if", static_cast(_te_if), TE_PURE}, + {"ifs", static_cast(_te_ifs), static_cast(TE_PURE|TE_VARIADIC)}, + {"ln", static_cast(_te_log), TE_PURE}, + {"log10", static_cast(_te_log10), TE_PURE}, + {"max", static_cast(_te_max), static_cast(TE_PURE|TE_VARIADIC)}, + {"min", static_cast(_te_min), static_cast(TE_PURE|TE_VARIADIC)}, + {"mod", static_cast(_te_modulus), TE_PURE}, + {"nan", static_cast(_te_nan_value), TE_PURE}, + {"ncr", static_cast(_te_ncr), TE_PURE}, + {"not", static_cast(_te_not), TE_PURE}, + {"npr", static_cast(_te_npr), TE_PURE}, + {"or", static_cast(_te_or_variadic), static_cast(TE_PURE|TE_VARIADIC)}, + {"permut", static_cast(_te_npr), TE_PURE}, + {"pi", static_cast(_te_pi), TE_PURE}, + {"pow", static_cast(_te_pow), TE_PURE}, + {"power",/* Excel alias*/ static_cast(_te_pow), TE_PURE}, + {"rand", static_cast(_te_random), TE_PURE}, + {"round", static_cast(_te_round), static_cast(TE_PURE|TE_VARIADIC)}, + {"sign", static_cast(_te_sign), TE_PURE}, + {"sin", static_cast(_te_sin), TE_PURE}, + {"sinh", static_cast(_te_sinh), TE_PURE}, + {"sqr", static_cast(_te_sqr), TE_PURE}, + {"sqrt", static_cast(_te_sqrt), TE_PURE}, + {"sum", static_cast(_te_sum), static_cast(TE_PURE|TE_VARIADIC)}, + {"tan", static_cast(_te_tan), TE_PURE}, + {"tanh", static_cast(_te_tanh), TE_PURE}, + {"tgamma", static_cast(_te_tgamma), TE_PURE}, + {"true", static_cast(_te_true_value), TE_PURE}, + {"trunc", static_cast(_te_trunc), TE_PURE} +}; + +//-------------------------------------------------- +void te_parser::next_token(te_parser::state *s) + { + assert(s); + if (!s) + { return; } + + s->m_type = te_parser::state::token_type::TOK_NULL; + + do + { + if (!*s->m_next) + { + s->m_type = te_parser::state::token_type::TOK_END; + return; + } + + /* Try reading a number. */ + if ((s->m_next[0] >= '0' && s->m_next[0] <= '9') || + s->m_next[0] == get_decimal_separator()) + { + char* nEnd{ nullptr }; + s->m_value = static_cast(std::strtod(s->m_next, &nEnd)); + s->m_next = nEnd; + s->m_type = te_parser::state::token_type::TOK_NUMBER; + } + else + { + /* Look for a variable or builtin function call. */ + if (is_letter(s->m_next[0]) || s->m_next[0] == '_') + { + const char* start = s->m_next; + while (is_name_char_valid(s->m_next[0])) + { s->m_next++; } + + m_varFound = false; + const std::string_view currentVarToken + { start, static_cast(s->m_next - start) }; + m_currentVar = find_lookup(s, currentVarToken); + if (m_currentVar != s->m_lookup.cend()) + { m_varFound = true; } + else + { + m_currentVar = find_builtin(currentVarToken); + if (m_currentVar != m_functions.cend()) + { m_varFound = true; } + // if unknown symbol resolve is not a no-op, then try using it + // to see what this variable is + else if (m_unknownSymbolResolve.index() != 0) + { + try + { + // "te_type usr(string_view)" resolver + if (m_unknownSymbolResolve.index() == 1) + { + const auto retUsrVal = std::get<1>(m_unknownSymbolResolve)(currentVarToken); + if (!std::isnan(retUsrVal)) + { + add_variable_or_function({ te_variable::name_type{ currentVarToken }, retUsrVal }); + m_currentVar = find_lookup(s, currentVarToken); + assert(m_currentVar != s->m_lookup.cend() && + "Internal error in parser using unknown symbol resolver."); + if (m_currentVar != s->m_lookup.cend()) + { + resolvedVariables.insert(te_variable::name_type{ currentVarToken }); + m_varFound = true; + } + } + } + // "te_type usr(string_view, string&)" resolver + else if (m_unknownSymbolResolve.index() == 2) + { + const auto retUsrVal = + std::get<2>(m_unknownSymbolResolve)(currentVarToken, m_lastErrorMessage); + if (!std::isnan(retUsrVal)) + { + add_variable_or_function({ te_variable::name_type{ currentVarToken }, retUsrVal }); + m_currentVar = find_lookup(s, currentVarToken); + assert(m_currentVar != s->m_lookup.cend() && + "Internal error in parser using unknown symbol resolver."); + if (m_currentVar != s->m_lookup.cend()) + { + resolvedVariables.insert(te_variable::name_type{ currentVarToken }); + m_varFound = true; + } + } + } + } + catch (const std::exception& exp) + { m_lastErrorMessage = exp.what(); } + } + } + + if (!m_varFound) + { + s->m_type = te_parser::state::token_type::TOK_ERROR; + } + else + { + // keep track of what's been used in the formula + if (is_function(m_currentVar->m_value) || + is_closure(m_currentVar->m_value)) + { m_usedFunctions.insert(m_currentVar->m_name); } + else + { m_usedVars.insert(m_currentVar->m_name); } + + if (is_constant(m_currentVar->m_value)) + { + s->m_type = te_parser::state::token_type::TOK_NUMBER; + s->m_value = m_currentVar->m_value; + } + else if (is_variable(m_currentVar->m_value)) + { + s->m_type = te_parser::state::token_type::TOK_VARIABLE; + s->m_value = m_currentVar->m_value; + } + else if (is_closure(m_currentVar->m_value)) + { + s->context = m_currentVar->m_context; + s->m_type = te_parser::state::token_type::TOK_FUNCTION; + s->m_varType = m_currentVar->m_type; + s->m_value = m_currentVar->m_value; + } + else if (is_function(m_currentVar->m_value)) + { + s->m_type = te_parser::state::token_type::TOK_FUNCTION; + s->m_varType = m_currentVar->m_type; + s->m_value = m_currentVar->m_value; + } + } + } + else + { + /* Look for an operator or special character. */ + const auto tok = s->m_next++[0]; + if (tok == '+') + { s->m_type = te_parser::state::token_type::TOK_INFIX; s->m_value = _te_add; } + else if (tok == '-') + { s->m_type = te_parser::state::token_type::TOK_INFIX; s->m_value = _te_sub; } + else if (tok == '*' && s->m_next[0] == '*') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_pow); + ++s->m_next; + } + else if (tok == '*') + { s->m_type = te_parser::state::token_type::TOK_INFIX; s->m_value = _te_mul; } + else if (tok == '/') + { s->m_type = te_parser::state::token_type::TOK_INFIX; s->m_value = _te_divide; } + else if (tok == '^') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_pow); + } + else if (tok == '%') + { s->m_type = te_parser::state::token_type::TOK_INFIX; s->m_value = _te_modulus; } + else if (tok == '(') + { s->m_type = te_parser::state::token_type::TOK_OPEN; } + else if (tok == ')') + { s->m_type = te_parser::state::token_type::TOK_CLOSE; } + else if (tok == get_list_separator()) + { s->m_type = te_parser::state::token_type::TOK_SEP; } + // shift operators + else if (tok == '<' && s->m_next[0] == '<') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_left_shift); + ++s->m_next; + } + else if (tok == '>' && s->m_next[0] == '>') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_right_shift); + ++s->m_next; + } + // logical operators + else if (tok == '=' && s->m_next[0] == '=') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_equal); + ++s->m_next; + } + else if (tok == '=') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_equal); + } + else if (tok == '!' && s->m_next[0] == '=') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_not_equal); + ++s->m_next; + } + else if (tok == '<' && s->m_next[0] == '>') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_not_equal); + ++s->m_next; + } + else if (tok == '<' && s->m_next[0] == '=') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_less_than_equal_to); + ++s->m_next; + } + else if (tok == '<') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_less_than); + } + else if (tok == '>' && s->m_next[0] == '=') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_greater_than_equal_to); + ++s->m_next; + } + else if (tok == '>') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_greater_than); + } + else if (tok == '&') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_and); + } + else if (tok == '|') + { + s->m_type = te_parser::state::token_type::TOK_INFIX; + s->m_value = static_cast(_te_or); + } + else if (tok == ' ' || tok == '\t' || tok == '\n' || tok == '\r') + { /*noop*/ } + else { s->m_type = te_parser::state::token_type::TOK_ERROR; } + } + } + } while (s->m_type == te_parser::state::token_type::TOK_NULL); + } + +//-------------------------------------------------- +te_expr* te_parser::base(te_parser::state *s) + { + /* = | | {"(" ")"} | | + "(" {"," } ")" | "(" ")" */ + te_expr* ret{ nullptr }; + + if (s->m_type == te_parser::state::token_type::TOK_OPEN) + { + next_token(s); + ret = list(s); + if (s->m_type != te_parser::state::token_type::TOK_CLOSE) + { s->m_type = te_parser::state::token_type::TOK_ERROR; } + else + { next_token(s); } + } + else if (s->m_type == te_parser::state::token_type::TOK_NUMBER) + { + ret = new_expr(TE_DEFAULT, s->m_value); + next_token(s); + } + else if (s->m_type == te_parser::state::token_type::TOK_VARIABLE) + { + ret = new_expr(TE_DEFAULT, s->m_value); + next_token(s); + } + else if (s->m_type == te_parser::state::token_type::TOK_NULL || + s->m_type == te_parser::state::token_type::TOK_ERROR || + s->m_type == te_parser::state::token_type::TOK_END || + s->m_type == te_parser::state::token_type::TOK_SEP || + s->m_type == te_parser::state::token_type::TOK_CLOSE || + s->m_type == te_parser::state::token_type::TOK_INFIX) + { + ret = new_expr(TE_DEFAULT, te_variant_type{ te_nan }); + s->m_type = te_parser::state::token_type::TOK_ERROR; + } + else if (is_function0(s->m_value) || is_closure0(s->m_value)) + { + ret = new_expr(s->m_varType, s->m_value, {}); + if (is_closure(s->m_value)) ret->m_parameters[0] = s->context; + next_token(s); + if (s->m_type == te_parser::state::token_type::TOK_OPEN) + { + next_token(s); + if (s->m_type != te_parser::state::token_type::TOK_CLOSE) + { s->m_type = te_parser::state::token_type::TOK_ERROR; } + else + { next_token(s); } + } + } + else if (is_function1(s->m_value) || is_closure1(s->m_value)) + { + ret = new_expr(s->m_varType, s->m_value); + if (is_closure(s->m_value)) + { ret->m_parameters[1] = s->context; } + next_token(s); + ret->m_parameters[0] = power(s); + } + else if (is_function2(s->m_value) || is_closure2(s->m_value) || + is_function3(s->m_value) || is_closure3(s->m_value) || + is_function4(s->m_value) || is_closure4(s->m_value) || + is_function5(s->m_value) || is_closure5(s->m_value) || + is_function6(s->m_value) || is_closure6(s->m_value) || + is_function7(s->m_value) || is_closure7(s->m_value)) + { + const int arity = get_arity(s->m_value); + + ret = new_expr(s->m_varType, s->m_value); + if (is_closure(s->m_value)) ret->m_parameters[arity] = s->context; + next_token(s); + + if (s->m_type != te_parser::state::token_type::TOK_OPEN) + { s->m_type = te_parser::state::token_type::TOK_ERROR; } + else + { + int i{ 0 }; + // If there are vars or other functions in the parameters, keep track of the original + // opening function; that is what we will do our variadic check on. + const bool varValid{ m_varFound }; + const auto openingVar = m_currentVar; + // load any parameters + for(i = 0; i < arity; i++) + { + next_token(s); + ret->m_parameters[i] = expr(s); + if(s->m_type != te_parser::state::token_type::TOK_SEP) + { break; } + } + if (s->m_type == te_parser::state::token_type::TOK_CLOSE && i != arity - 1 && + varValid && is_variadic(openingVar->m_type)) + { next_token(s); } + else if(s->m_type != te_parser::state::token_type::TOK_CLOSE || i != arity - 1) + { s->m_type = te_parser::state::token_type::TOK_ERROR; } + else + { next_token(s); } + } + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::list(te_parser::state *s) + { + /* = {"," } */ + te_expr* ret = expr(s); + + while (s->m_type == te_parser::state::token_type::TOK_SEP) { + next_token(s); + ret = new_expr(TE_PURE, te_variant_type(_comma), { ret, expr(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::expr(te_parser::state *s) + { + /* = {(logic operations) } */ + // These are the lowest of operator precedence + // (once we have split tokens into arguments) + te_expr* ret = expr_level2(s); + + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_and || + get_function2(s->m_value) == _te_or)) + { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, expr_level2(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::expr_level2(te_parser::state *s) + { + /* = {(comparison operators) } */ + // Second from the lowest of operator precedence + te_expr* ret = expr_level3(s); + + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_equal || + get_function2(s->m_value) == _te_not_equal || + get_function2(s->m_value) == _te_less_than || + get_function2(s->m_value) == _te_less_than_equal_to || + get_function2(s->m_value) == _te_greater_than || + get_function2(s->m_value) == _te_greater_than_equal_to)) + { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, expr_level3(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::expr_level3(te_parser::state *s) + { + /* = {("<<" | ">>") } */ + // Third from the lowest level of operator precendence + te_expr* ret = expr_level4(s); + + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_left_shift || + get_function2(s->m_value) == _te_right_shift)) + { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, expr_level4(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::expr_level4(te_parser::state *s) + { + /* = {("+" | "-") } */ + // Fourth from the lowest level of operator precendence + te_expr* ret = term(s); + + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_add || + get_function2(s->m_value) == _te_sub)) + { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, term(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_expr* te_parser::term(te_parser::state *s) + { + /* = {("*" | "/" | "%") } */ + te_expr* ret = factor(s); + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_mul || + get_function2(s->m_value) == _te_divide || + get_function2(s->m_value) == _te_modulus)) { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, factor(s) }); + } + + return ret; + } + +#ifdef TE_POW_FROM_RIGHT +te_expr* te_parser::factor(te_parser::state *s) { + /* = {"^" } */ + te_expr* ret = power(s); + + int neg{ 0 }; + + if (ret->m_type == TE_PURE && + is_function1(ret->m_value) && + get_function1(ret->m_value) == _te_negate) { + te_expr *se = ret->m_parameters[0]; + delete ret; + ret = se; + neg = 1; + } + + te_expr* insertion{ nullptr }; + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == static_cast(_te_pow))) { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + + if (insertion) { + /* Make exponentiation go right-to-left. */ + te_expr* insert = new_expr(TE_PURE, t, { insertion->m_parameters[1], power(s) }); + insertion->m_parameters[1] = insert; + insertion = insert; + } else { + ret = new_expr(TE_PURE, t, { ret, power(s) }); + insertion = ret; + } + } + + if (neg) { + ret = new_expr(TE_PURE, te_variant_type(_te_negate), { ret }); + } + + return ret; +} +#else +te_expr* te_parser::factor(te_parser::state *s) + { + /* = {"^" } */ + te_expr* ret = power(s); + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == static_cast(_te_pow))) { + const te_fun2 t = get_function2(s->m_value); + next_token(s); + ret = new_expr(TE_PURE, t, { ret, power(s) }); + } + + return ret; + } +#endif + +//-------------------------------------------------- +te_expr* te_parser::power(te_parser::state *s) + { + /* = {("-" | "+")} */ + int Sign{ 1 }; + while (s->m_type == te_parser::state::token_type::TOK_INFIX && + is_function2(s->m_value) && + (get_function2(s->m_value) == _te_add || + get_function2(s->m_value) == _te_sub)) + { + if (get_function2(s->m_value) == _te_sub) Sign = -Sign; + next_token(s); + } + + te_expr* ret{ nullptr }; + + if (Sign == 1) { + ret = base(s); + } else { + ret = new_expr(TE_PURE, te_variant_type(_te_negate), { base(s) }); + } + + return ret; + } + +//-------------------------------------------------- +te_type te_parser::te_eval(const te_expr *n) + { + if (!n) return te_nan; + + // cppcheck-suppress unreadVariable + const auto M = [&n = std::as_const(n)](const size_t e) + { + return (e < n->m_parameters.size()) ? te_eval(n->m_parameters[e]) : + te_nan; + }; + + switch (n->m_value.index()) + { + case 0: + return get_constant(n->m_value); + case 1: + return *(get_variable(n->m_value)); + case 2: + return get_function0(n->m_value)(); + case 3: + return get_function1(n->m_value)(M(0)); + case 4: + return get_function2(n->m_value)(M(0), M(1)); + case 5: + return get_function3(n->m_value)(M(0), M(1), M(2)); + case 6: + return get_function4(n->m_value)(M(0), M(1), M(2), M(3)); + case 7: + return get_function5(n->m_value)(M(0), M(1), M(2), M(3), M(4)); + case 8: + return get_function6(n->m_value)(M(0), M(1), M(2), M(3), M(4), M(5)); + case 9: + return get_function7(n->m_value)(M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + case 10: + return get_closure0(n->m_value)(n->m_parameters[0]); + case 11: + return get_closure1(n->m_value)(n->m_parameters[1], M(0)); + case 12: + return get_closure2(n->m_value)(n->m_parameters[2], M(0), M(1)); + case 13: + return get_closure3(n->m_value)(n->m_parameters[3], M(0), M(1), M(2)); + case 14: + return get_closure4(n->m_value)(n->m_parameters[4], M(0), M(1), M(2), M(3)); + case 15: + return get_closure5(n->m_value)(n->m_parameters[5], M(0), M(1), M(2), M(3), M(4)); + case 16: + return get_closure6(n->m_value)(n->m_parameters[6], M(0), M(1), M(2), M(3), M(4), M(5)); + case 17: + return get_closure7(n->m_value)(n->m_parameters[7], M(0), M(1), M(2), M(3), M(4), M(5), M(6)); + default: + return te_nan; + }; + } + +//-------------------------------------------------- +void te_parser::optimize(te_expr *n) + { + if (!n) return; + /* Evaluates as much as possible. */ + if (is_constant(n->m_value) || is_variable(n->m_value)) return; + + /* Only optimize out functions flagged as pure. */ + if (is_pure(n->m_type)) + { + const int arity = get_arity(n->m_value); + bool known{ true }; + for (int i = 0; i < arity; ++i) + { + if (!n->m_parameters[i]) + { break; } + optimize(n->m_parameters[i]); + if (!is_constant(n->m_parameters[i]->m_value)) + { known = false; } + } + if (known) + { + const auto value = te_eval(n); + te_free_parameters(n); + n->m_type = TE_DEFAULT; + n->m_value = value; + } + } + } + +//-------------------------------------------------- +te_expr* te_parser::te_compile(const std::string_view expression, std::set& variables) + { + state s(expression.data(), TE_DEFAULT, variables); + + next_token(&s); + te_expr* root = list(&s); + + if (s.m_type != te_parser::state::token_type::TOK_END) + { + te_free(root); + m_errorPos = (s.m_next - s.m_start); + if (m_errorPos > 0) --m_errorPos; + return nullptr; + } + else + { + optimize(root); + m_errorPos = te_parser::npos; + return root; + } + } + +//-------------------------------------------------- +bool te_parser::compile(const std::string_view expression) + { + // reset everything from previous call + m_errorPos = te_parser::npos; + m_lastErrorMessage.clear(); + m_result = te_nan; + m_parseSuccess = false; + te_free(m_compiledExpression); + m_compiledExpression = nullptr; + m_currentVar = m_functions.cend(); + m_varFound = false; + m_usedFunctions.clear(); + m_usedVars.clear(); + resolvedVariables.clear(); + if (get_list_separator() == get_decimal_separator()) + { throw std::runtime_error("List and decimal separators cannot be the same"); } + if (expression.empty()) + { + m_expression.clear(); + m_errorPos = 0; + return false; + } + m_expression.assign(expression); + + // In case the expression was a spreadsheet formula like "=SUM(...)", + // remove the '=' in front. + if (m_expression.length() && m_expression.front() == '=') + { m_expression.erase(0, 1); } + + // remove multi-line comments + size_t commentStart{ 0 }; + while (commentStart != std::string::npos) + { + commentStart = m_expression.find("/*", commentStart); + if (commentStart == std::string::npos) + { break; } + auto commentEnd = m_expression.find("*/", commentStart); + if (commentEnd == std::string::npos) + { + m_errorPos = commentStart; + return te_nan; + } + m_expression.erase(commentStart, (commentEnd + 2) - commentStart); + } + // remove single-line comments + commentStart = 0; + while (commentStart != std::string::npos) + { + commentStart = m_expression.find("//", commentStart); + if (commentStart == std::string::npos) + { break; } + auto commentEnd = m_expression.find_first_of("\n\r", commentStart); + if (commentEnd == std::string::npos) + { + m_expression.erase(commentStart); + break; + } + else + { m_expression.erase(commentStart, commentEnd - commentStart); } + } + + try + { + m_compiledExpression = te_compile(m_expression, get_variables_and_functions()); + m_parseSuccess = (m_compiledExpression != nullptr) ? true : false; + } + catch (const std::exception& expt) + { + m_parseSuccess = false; + m_result = te_nan; + m_lastErrorMessage = expt.what(); + } + + reset_usr_resolved_if_necessary(); + + return m_parseSuccess; + } + +//-------------------------------------------------- +te_type te_parser::evaluate() + { + try + { + m_result = (m_compiledExpression) ? + te_eval(m_compiledExpression) : + te_nan; + } + catch (const std::exception& expt) + { + m_parseSuccess = false; + m_result = te_nan; + m_lastErrorMessage = expt.what(); + } + + reset_usr_resolved_if_necessary(); + + return m_result; + } + +//-------------------------------------------------- +te_type te_parser::evaluate(const std::string_view expression) + { + if (compile(expression)) + { return evaluate(); } + else + { return te_nan; } + } + +//-------------------------------------------------- +// cppcheck-suppress unusedFunction +std::string te_parser::list_available_functions_and_variables() + { + std::string report = "Built-in Functions:\n"; + for (const auto& func : m_functions) + { report.append(func.m_name).append("\n"); } + report.append("\nCustom Functions & Variables:\n"); + for (const auto& func : get_variables_and_functions()) + { report.append(func.m_name).append("\n"); } + return report; + } \ No newline at end of file diff --git a/src/tinyexpr-plusplus/tinyexpr.h b/src/tinyexpr-plusplus/tinyexpr.h new file mode 100644 index 0000000000000000000000000000000000000000..8278fb4189825096e40b4ee27d514188f33ea0ed --- /dev/null +++ b/src/tinyexpr-plusplus/tinyexpr.h @@ -0,0 +1,851 @@ +// SPDX-License-Identifier: Zlib +/* + * TINYEXPR - Tiny recursive descent parser and evaluation engine in C + * + * Copyright (c) 2015-2020 Lewis Van Winkle + * + * http://CodePlea.com + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +/* + * TINYEXPR++ - Tiny recursive descent parser and evaluation engine in C++ + * + * Copyright (c) 2020-2023 Blake Madden + * + * C++ version of the TinyExpr library. + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgement in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifndef __TINYEXPR_PLUS_PLUS_H__ +#define __TINYEXPR_PLUS_PLUS_H__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class te_parser; + +/// @brief Define this to use @c float instead of @c double for the parser's data type. +#ifdef TE_FLOAT + /// @brief The parameter and return type for parser and its functions. + using te_type = float; +#else + /// @brief The parameter and return type for parser and its functions. + using te_type = double; +#endif + +class te_expr; + +// regular functions +using te_fun0 = te_type (*)(); +using te_fun1 = te_type (*)(te_type); +using te_fun2 = te_type (*)(te_type, te_type); +using te_fun3 = te_type (*)(te_type, te_type, te_type); +using te_fun4 = te_type (*)(te_type, te_type, te_type, te_type); +using te_fun5 = te_type (*)(te_type, te_type, te_type, te_type, te_type); +using te_fun6 = te_type (*)(te_type, te_type, te_type, te_type, te_type, te_type); +using te_fun7 = te_type (*)(te_type, te_type, te_type, te_type, te_type, te_type, te_type); +// context functions (where te_variable passes a client's te_expr as the first argument) +using te_confun0 = te_type (*)(const te_expr*); +using te_confun1 = te_type (*)(const te_expr*, te_type); +using te_confun2 = te_type (*)(const te_expr*, te_type, te_type); +using te_confun3 = te_type (*)(const te_expr*, te_type, te_type, te_type); +using te_confun4 = te_type (*)(const te_expr*, te_type, te_type, te_type, te_type); +using te_confun5 = te_type (*)(const te_expr*, te_type, te_type, te_type, te_type, te_type); +using te_confun6 = te_type (*)(const te_expr*, te_type, te_type, te_type, te_type, te_type, te_type); +using te_confun7 = te_type (*)(const te_expr*, te_type, te_type, te_type, te_type, te_type, te_type, te_type); + +// functions for unknown symbol resolution +using te_usr_noop = std::function; +using te_usr_fun0 = std::function; +using te_usr_fun1 = std::function; + +using te_usr_variant_type = std::variant; + +// do not change the ordering of these, the indices are used to determine the value type of a te_variable +using te_variant_type = std::variant; + +/// @brief A variable's flags, effecting how it is evaluated. +/// @note This is a bitmask, so flags (TE_PURE and TE_VARIADIC) can be OR'ed. +/// @internal Note that because this is a bitmask, don't declare it as an enum class, +/// just a C-style enum. +enum te_variable_flags + { + /// @brief Don't do anything special when evaluating. + TE_DEFAULT = 0, + /// @brief Don't update when simple evaluation is ran + /// (i.e., only updated when expression is compiled). + TE_PURE = (1 << 0), + /// @brief Function that can take 1-7 argument (unused arguments are set to NaN). + TE_VARIADIC = (1 << 1) + }; + +/// @private +class te_string_less + { +public: + [[nodiscard]] + bool operator()(const std::string& lhv, const std::string& rhv) const + { + const auto minStrLen = std::min(lhv.length(), rhv.length()); + for (size_t i = 0; i < minStrLen; ++i) + { + const auto lhCh = tolower(lhv[i]); + const auto rhCh = tolower(rhv[i]); + if (lhCh == rhCh) + { continue; } + return (lhCh < rhCh); + } + return (lhv.length() < rhv.length()); + } + // We can assume that we are only dealing with a-z, A-Z, 0-9, ., or _, + // so use a branchless tolower. + [[nodiscard]] + constexpr static char tolower(const char ch) noexcept + { return ch + (32 * (ch >= 'A' && ch <= 'Z')); } + }; + +/// @brief A compiled expression. +/// @details Can also be an additional object that can be passed to +/// te_confun0-te_confun7 functions via a te_variable. +class te_expr + { +public: + te_expr(const te_variable_flags type, const te_variant_type& value) noexcept : + m_type(type), m_value(value) {} + explicit te_expr(const te_variable_flags type) noexcept : m_type(type) {} + /// @private + te_expr() noexcept {}; + /// @private + te_expr(const te_expr&) = delete; + /// @private + te_expr& operator=(const te_expr&) = delete; + /// @private + virtual ~te_expr() {} + /// @brief The type that m_value represents. + te_variable_flags m_type{ TE_DEFAULT }; + /// @brief The te_type constant, te_type pointer, or function to bind to. + te_variant_type m_value{ static_cast(0.0) }; + /// @brief Additional parameters. + std::vector m_parameters{ nullptr }; + }; + +/// @brief Custom variable or function that can be added to a te_parser. +class te_variable + { +public: + /// @private + using name_type = std::string; + /// @private + [[nodiscard]] + bool operator<(const te_variable& that) const + { return te_string_less{}(m_name, that.m_name); } + /// @brief The name as it would appear in a formula. + name_type m_name; + /// @brief The te_type constant, te_type pointer, or function to bind the name to. + te_variant_type m_value; + /// @brief The type that m_value represents. + te_variable_flags m_type{ TE_DEFAULT }; + /// If @c m_value is a function pointer of type `te_confun0`-`te_confun7`, then + /// this is passed to that function when called. This is useful for passing + /// an object which manages additional data to your functions. + te_expr* m_context{ nullptr }; + }; + +/// @brief Math formula parser. +class te_parser + { +public: + /// @private + te_parser() = default; + /// @private + te_parser(const te_parser&) = delete; + /// @private + te_parser& operator=(const te_parser&) = delete; + /// @private + ~te_parser() + { te_free(m_compiledExpression); } + /// @brief NaN (not-a-number) constant to indicate an invalid value. + static constexpr auto te_nan = std::numeric_limits::quiet_NaN(); + /// @brief No position, which is what get_last_error_position() returns + /// when there was no parsing error. + static constexpr int64_t npos = -1; + /** @brief Parses the input @c expression. + @param expression The formula to compile. + @returns Whether the expression compiled or not. (This can be checked + by calling success() afterwards as well.) + @sa success(). + @note Returns NaN if division or modulus by zero occurs. + @throws std::runtime_error Throws an exception in the case of arithmetic overflows + (e.g., `1 << 64` would cause an overflow).*/ + bool compile(const std::string_view expression); + /** @brief Evaluates expression passed to compile() previously and returns its result. + @returns The result, or NaN on error. + @throws std::runtime_error Throws an exception in the case of arithmetic overflows + (e.g., `1 << 64` would cause an overflow).*/ + [[nodiscard]] + te_type evaluate(); + /** @brief Compiles and evaluates an expression and returns its result. + @param expression The formula to compile and evaluate. + @returns The result, or NaN on error. + @note Returns NaN if division or modulus by zero occurs. + @throws std::runtime_error Throws an exception in the case of arithmetic overflows + (e.g., `1 << 64` would cause an overflow).*/ + [[nodiscard]] + te_type evaluate(const std::string_view expression); + /// @returns The last call to evaluate()'s result (which will be NaN on error). + [[nodiscard]] + te_type get_result() const noexcept + { return m_result; } + /// @private + [[nodiscard]] + te_type get_result() const volatile noexcept + { return m_result; } + /// @returns Whether the last call to compile() was successful. + /// @sa get_last_error_position(). + [[nodiscard]] + bool success() const noexcept + { return m_parseSuccess; } + [[nodiscard]] + bool success() const volatile noexcept + { return m_parseSuccess; } + + /// @returns The zero-based index into the last parsed expression where the parse failed, + /// or te_parser::npos if no error occurred. + /// @note Call success() to see if the last parse succeeded or not. + [[nodiscard]] + int64_t get_last_error_position() const noexcept + { return m_errorPos; } + /// @private + [[nodiscard]] + int64_t get_last_error_position() const volatile noexcept + { return m_errorPos; } + + /// @returns Any error message from the last parse. + [[nodiscard]] + const std::string& get_last_error_message() const noexcept + { return m_lastErrorMessage; } + + /// @brief Sets the list of custom variables and functions. + /// @param vars The list of variables and functions. + /// @note Valid variable and function names must begin with a letter from a-z (A-Z), + /// followed by additional English letters, numbers, periods, or underscores. + /// @throws std::runtime_error Throws an exception if an illegal character is found + /// in any variable name. + void set_variables_and_functions(std::set vars) + { + for (const auto& var : vars) + { validate_name(var); } + m_customFuncsAndVars = std::move(vars); + } + /// @brief Adds a custom variable or function. + /// @param var The variable/function to add. + /// @note Prefer using set_variables_and_functions() as it will be more optimal + /// (less sorts will need to be performed). + /// @throws std::runtime_error Throws an exception if an illegal character is found + /// in the variable name. + void add_variable_or_function(te_variable var) + { + validate_name(var); + m_customFuncsAndVars.insert(std::move(var)); + } + /// @brief Removes a custom variable or function. + /// @param var The variable/function to remove (by name). + void remove_variable_or_function(te_variable::name_type var) + { + auto foundVar = m_customFuncsAndVars.find( + te_variable{ std::move(var), static_cast(0.0), TE_DEFAULT, nullptr }); + if (foundVar != m_customFuncsAndVars.cend()) + { m_customFuncsAndVars.erase(foundVar); } + } + /** @brief Sets a custom function to resolve unknown symbols in an expression. + @param usr The function to use to resolve unknown symbols. + @param keepResolvedVariables @c true to cache any resolved variables into the parser. + This means that they will not need to be resolved again on subsequent calls to evaluate().\n + Pass @c false to this if you wish to re-resolve any previously resolved variables on + later evaluations. This can be useful for when a resolved variable's value is volatile + and needs to be re-resolved on every use.*/ + void set_unknown_symbol_resolver(te_usr_variant_type usr, + const bool keepResolvedVariables = true) + { + m_unknownSymbolResolve = usr; + m_keepResolvedVarialbes = keepResolvedVariables; + } + /// @private + [[nodiscard]] + const std::set& get_variables_and_functions() const noexcept + { return m_customFuncsAndVars; } + /// @returns The list of custom variables and functions. + [[nodiscard]] + std::set& get_variables_and_functions() noexcept + { return m_customFuncsAndVars; } + + /// @returns The decimal separator used for numbers. + [[nodiscard]] + char get_decimal_separator() const noexcept + { return m_decimalSeparator; } + /// @private + [[nodiscard]] + char get_decimal_separator() const volatile noexcept + { return m_decimalSeparator; } + /// @brief Sets the decimal separator used for numbers. + /// @param sep The decimal separator. + /// @throws std::runtime_error Throws an exception if an illegal character is used. + void set_decimal_separator(const char sep) + { + if (sep != ',' && sep != '.') + { throw std::runtime_error("Decimal separator must be either a '.' or ','."); } + m_decimalSeparator = sep; + } + /// @private + void set_decimal_separator(const char sep) volatile + { + if (sep != ',' && sep != '.') + { throw std::runtime_error("Decimal separator must be either a '.' or ','."); } + m_decimalSeparator = sep; + } + + /// @brief Sets a constant variable's value. + /// @param name The name of the (constant) variable. + /// @param value The new value to set the constant to. + /// @note If the constant variable hasn't been added yet (via set_variables_and_functions()), + /// then this will add it.\n + /// If a variable with the provided name is found but is not a constant, + /// then this will be ignored. + void set_constant(const std::string_view name, const te_type value) + { + auto cvar = find_variable_or_function(name); + if (cvar == get_variables_and_functions().end()) + { + add_variable_or_function({ te_variable::name_type{ name}, value }); + } + else if (is_constant(cvar->m_value)) + { + auto nh = get_variables_and_functions().extract(cvar); + nh.value().m_value = value; + get_variables_and_functions().insert(std::move(nh)); + // if previously compiled, then re-compile since this + // constant would have been optimized + if (m_expression.length()) + { compile(m_expression); } + } + } + /// @brief Retrieves a constant variable's value. + /// @param name The name of the (constant) variable. + /// @returns The value of the constant variable if found, NaN otherwise. + [[nodiscard]] + te_type get_constant(const std::string_view name) const + { + auto cvar = find_variable_or_function(name); + if (cvar == get_variables_and_functions().cend() || !is_constant(cvar->m_value)) + { return te_nan; } + if (const auto val = std::get_if(&cvar->m_value); + val != nullptr) + { return *val; } + else + { return te_nan; } + } + + /// @returns The separator used between function arguments. + [[nodiscard]] + char get_list_separator() const noexcept + { return m_listSeparator; } + /// @private + [[nodiscard]] + char get_list_separator() const volatile noexcept + { return m_listSeparator; } + /// @brief Sets the separator used between function arguments. + /// @param sep The list separator. + /// @throws std::runtime_error Throws an exception if an illegal character is used. + void set_list_separator(const char sep) + { + if (sep != ',' && sep != ';') + { throw std::runtime_error("List separator must be either a ',' or ';'."); } + m_listSeparator = sep; + } + /// @private + void set_list_separator(const char sep) volatile + { + if (sep != ',' && sep != ';') + { throw std::runtime_error("List separator must be either a ',' or ';'."); } + m_listSeparator = sep; + } + + /// @returns @c true if @c name is a function that had been used in the last parsed formula. + /// @param name The name of the function. + /// @sa compile() and evaluate(). + [[nodiscard]] + bool is_function_used(const std::string_view name) const + { + return m_usedFunctions.find( + te_variable::name_type{ name }) != m_usedFunctions.cend(); + } + /// @returns @c true if @c name is a variable that had been used in the last parsed formula. + /// @param name The name of the variable. + /// @sa compile() and evaluate(). + [[nodiscard]] + bool is_variable_used(const std::string_view name) const + { + return m_usedVars.find( + te_variable::name_type{ name }) != m_usedVars.cend(); + } + + /// @returns A report of all available functions and variables. + [[nodiscard]] + std::string list_available_functions_and_variables(); + + /// @returns The last formula passed to the parser. + /// @note Comments will be stripped from the original expression. + [[nodiscard]] + const std::string& get_expression() const noexcept + { return m_expression; }; +private: + /// @brief Resets any resolved variables from USR if not being cached. + void reset_usr_resolved_if_necessary() + { + if (!m_keepResolvedVarialbes && resolvedVariables.size()) + { + for (const auto& resolvedVar : resolvedVariables) + { remove_variable_or_function(resolvedVar); } + resolvedVariables.clear(); + } + } + /// @brief Gets the compiled expression, which will the optimized version + /// of the original expression. + /// @returns The compiled expression. + [[nodiscard]] + const te_expr* get_compiled_expression() const noexcept + { return m_compiledExpression; } + /// @private + [[nodiscard]] + const te_expr* get_compiled_expression() const volatile noexcept + { return m_compiledExpression; } + /// @brief Validates that a variable only contains legal characters + /// (and has a valid length). + /// @param var The variable to validate. + /// @throws std::runtime_error Throws an exception if an illegal character is found. + void validate_name(const te_variable& var) const + { + if (var.m_name.empty()) + { throw std::runtime_error("Variable name is empty."); } + if (!is_letter(var.m_name[0]) && + var.m_name[0] != '_') + { + throw std::runtime_error( + std::string("Variable name must begin with a letter from a-z or _: ") + var.m_name); + } + const auto varCharPos = std::find_if(var.m_name.cbegin(), var.m_name.cend(), + [](const auto ch) noexcept + { + return !is_name_char_valid(ch); + }); + if (varCharPos != var.m_name.cend()) + { + throw std::runtime_error( + std::string("Invalid character in variable name: ") + var.m_name); + } + } + /// @returns @c true if character is valid for a function or variable name. + /// @param ch The character to review. + [[nodiscard]] + static constexpr bool is_name_char_valid(const char ch) noexcept + { + return (is_letter(ch) || + (ch >= '0' && ch <= '9') || + (ch == '_') || (ch == '.')); + } + /// @returns An iterator to the custom variable or function with the given @c name, + /// or end of get_variables_and_functions() if not found. + /// @param name The name of the function or variable to search for. + [[nodiscard]] + std::set::iterator find_variable_or_function(const std::string_view name) + { + if (name.empty()) + { return m_customFuncsAndVars.end(); } + + return m_customFuncsAndVars.find( + te_variable{ te_variable::name_type{ name }, static_cast(0.0), TE_DEFAULT, nullptr }); + } + + /// @returns An iterator to the custom variable or function with the given @c name, + /// or end of get_variables_and_functions() if not found. + /// @param name The name of the function or variable to search for. + [[nodiscard]] + std::set::const_iterator find_variable_or_function(const std::string_view name) const + { + if (name.empty()) + { return m_customFuncsAndVars.cend(); } + + return m_customFuncsAndVars.find( + te_variable{ te_variable::name_type{ name }, static_cast(0.0), TE_DEFAULT, nullptr }); + } + + [[nodiscard]] + constexpr static auto is_pure(const te_variable_flags type) + { return (((type)&TE_PURE) != 0); } + [[nodiscard]] + constexpr static auto is_variadic(const te_variable_flags type) + { return (((type)&TE_VARIADIC) != 0); } + /// @returns Number of parameters that a function/variable takes. + [[nodiscard]] + inline static auto get_arity(const te_variant_type& var) noexcept + { + return (var.index() == 0 || var.index() == 1) ? 0 : + (is_function0(var) || is_closure0(var)) ? 0 : + (is_function1(var) || is_closure1(var)) ? 1 : + (is_function2(var) || is_closure2(var)) ? 2 : + (is_function3(var) || is_closure3(var)) ? 3 : + (is_function4(var) || is_closure4(var)) ? 4 : + (is_function5(var) || is_closure5(var)) ? 5 : + (is_function6(var) || is_closure6(var)) ? 6 : + (is_function7(var) || is_closure7(var)) ? 7 : + 0; + } + [[nodiscard]] + constexpr static bool is_constant(const te_variant_type& var) noexcept + { return var.index() == 0; } + [[nodiscard]] + constexpr static te_type get_constant(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<0>(var); + } + [[nodiscard]] + constexpr static bool is_variable(const te_variant_type& var) noexcept + { return var.index() == 1; } + [[nodiscard]] + constexpr static const te_type* get_variable(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<1>(var); + } + [[nodiscard]] + constexpr static bool is_function(const te_variant_type& var) noexcept + { return (var.index() >= 2 && var.index() <= 9); } + [[nodiscard]] + constexpr static bool is_function0(const te_variant_type& var) noexcept + { return var.index() == 2; } + [[nodiscard]] + constexpr static te_fun0 get_function0(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<2>(var); + } + [[nodiscard]] + constexpr static bool is_function1(const te_variant_type& var) noexcept + { return var.index() == 3; } + [[nodiscard]] + constexpr static te_fun1 get_function1(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<3>(var); + } + [[nodiscard]] + constexpr static bool is_function2(const te_variant_type& var) noexcept + { return var.index() == 4; } + [[nodiscard]] + constexpr static te_fun2 get_function2(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<4>(var); + } + [[nodiscard]] + constexpr static bool is_function3(const te_variant_type& var) noexcept + { return var.index() == 5; } + [[nodiscard]] + constexpr static te_fun3 get_function3(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<5>(var); + } + [[nodiscard]] + constexpr static bool is_function4(const te_variant_type& var) noexcept + { return var.index() == 6; } + [[nodiscard]] + constexpr static te_fun4 get_function4(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<6>(var); + } + [[nodiscard]] + constexpr static bool is_function5(const te_variant_type& var) noexcept + { return var.index() == 7; } + [[nodiscard]] + constexpr static te_fun5 get_function5(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<7>(var); + } + [[nodiscard]] + constexpr static bool is_function6(const te_variant_type& var) noexcept + { return var.index() == 8; } + [[nodiscard]] + constexpr static te_fun6 get_function6(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<8>(var); + } + [[nodiscard]] + constexpr static bool is_function7(const te_variant_type& var) noexcept + { return var.index() == 9; } + [[nodiscard]] + constexpr static te_fun7 get_function7(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<9>(var); + } + [[nodiscard]] + constexpr static bool is_closure(const te_variant_type& var) noexcept + { return (var.index() >= 10 && var.index() <= 17); } + [[nodiscard]] + constexpr static bool is_closure0(const te_variant_type& var) noexcept + { return var.index() == 10; } + [[nodiscard]] + constexpr static te_confun0 get_closure0(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<10>(var); + } + [[nodiscard]] + constexpr static bool is_closure1(const te_variant_type& var) noexcept + { return var.index() == 11; } + [[nodiscard]] + constexpr static te_confun1 get_closure1(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<11>(var); + } + [[nodiscard]] + constexpr static bool is_closure2(const te_variant_type& var) noexcept + { return var.index() == 12; } + [[nodiscard]] + constexpr static te_confun2 get_closure2(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<12>(var); + } + [[nodiscard]] + constexpr static bool is_closure3(const te_variant_type& var) noexcept + { return var.index() == 13; } + [[nodiscard]] + constexpr static te_confun3 get_closure3(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<13>(var); + } + [[nodiscard]] + constexpr static bool is_closure4(const te_variant_type& var) noexcept + { return var.index() == 14; } + [[nodiscard]] + constexpr static te_confun4 get_closure4(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<14>(var); + } + [[nodiscard]] + constexpr static bool is_closure5(const te_variant_type& var) noexcept + { return var.index() == 15; } + [[nodiscard]] + constexpr static te_confun5 get_closure5(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<15>(var); + } + [[nodiscard]] + constexpr static bool is_closure6(const te_variant_type& var) noexcept + { return var.index() == 16; } + [[nodiscard]] + constexpr static te_confun6 get_closure6(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<16>(var); + } + [[nodiscard]] + constexpr static bool is_closure7(const te_variant_type& var) noexcept + { return var.index() == 17; } + [[nodiscard]] + constexpr static te_confun7 get_closure7(const te_variant_type& var) + { + assert(std::holds_alternative(var)); + return std::get<17>(var); + } + + struct state + { + enum class token_type + { + TOK_NULL, TOK_ERROR, TOK_END, TOK_SEP, TOK_OPEN, + TOK_CLOSE, TOK_NUMBER, TOK_VARIABLE, TOK_FUNCTION, TOK_INFIX + }; + state(const char* expression, te_variable_flags varType, + std::set& vars) : + m_start(expression), m_next(expression), + m_varType(varType), m_lookup(vars) + {} + const char* m_start{ nullptr }; + const char* m_next{ nullptr }; + token_type m_type{ token_type::TOK_NULL }; + te_variable_flags m_varType{ TE_DEFAULT }; + te_variant_type m_value; + te_expr* context{ nullptr }; + + std::set& m_lookup; + }; + [[nodiscard]] + static inline te_expr* new_expr(const te_variable_flags type, + te_variant_type value, const std::initializer_list& parameters) + { + te_expr* ret = new te_expr{ type, std::move(value) }; + ret->m_parameters.resize( + std::max( + std::max(parameters.size(), get_arity(ret->m_value)) + + (is_closure(ret->m_value) ? 1 : 0), + 0) + ); + if (parameters.size()) + { std::copy(parameters.begin(), parameters.end(), ret->m_parameters.begin()); } + return ret; + } + [[nodiscard]] + static inline te_expr* new_expr(const te_variable_flags type, te_variant_type value) + { + te_expr* ret = new te_expr{ type, std::move(value) }; + ret->m_parameters.resize(static_cast(get_arity(ret->m_value)) + + (is_closure(ret->m_value) ? 1 : 0)); + return ret; + } + [[nodiscard]] + static constexpr bool is_letter(const char ch) noexcept + { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } + /** @brief Parses the input expression and binds variables. + @param expression The formula to parse. + @param variables The collection of custom functions and + variables to add to the parser. + @returns null on error.*/ + [[nodiscard]] + te_expr* te_compile(const std::string_view expression, + std::set& variables); + /* Evaluates the expression. */ + [[nodiscard]] + static te_type te_eval(const te_expr* n); + /* Frees the expression. */ + /* This is safe to call on null pointers. */ + static inline void te_free(te_expr* n) + { + if (!n) return; + te_free_parameters(n); + delete n; + } + static void te_free_parameters(te_expr* n); + static void optimize(te_expr* n); + [[nodiscard]] + static auto find_builtin(const std::string_view name) + { + return m_functions.find( + te_variable{ te_variable::name_type{ name }, static_cast(0.0), TE_DEFAULT, nullptr }); + } + + [[nodiscard]] + static auto find_lookup(state* s, const std::string_view name) + { + return s->m_lookup.find( + te_variable{ te_variable::name_type{ name }, static_cast(0.0), TE_DEFAULT, nullptr }); + } + + void next_token(state* s); + [[nodiscard]] + te_expr* base(state* s); + [[nodiscard]] + te_expr* power(state* s); + [[nodiscard]] + te_expr* factor(state* s); + [[nodiscard]] + te_expr* term(state* s); + [[nodiscard]] + te_expr* expr(state* s); + [[nodiscard]] + te_expr* expr_level2(state* s); + [[nodiscard]] + te_expr* expr_level3(state* s); + [[nodiscard]] + te_expr* expr_level4(state* s); + [[nodiscard]] + te_expr* list(state* s); + + std::string m_expression; + te_expr* m_compiledExpression{ nullptr }; + + std::set::const_iterator m_currentVar; + bool m_varFound{ false }; + std::set m_usedFunctions; + std::set m_usedVars; + + static const std::set m_functions; + std::set m_customFuncsAndVars; + + te_usr_variant_type m_unknownSymbolResolve{ te_usr_noop{} }; + std::set resolvedVariables; + bool m_keepResolvedVarialbes{ true }; + + // just keeps track of built-in operators + static const std::set m_operators; + + bool m_parseSuccess{ false }; + int64_t m_errorPos{ 0 }; + std::string m_lastErrorMessage; + te_type m_result{ te_nan }; + char m_decimalSeparator{ '.' }; + char m_listSeparator{ ',' }; + }; + +#endif // __TINYEXPR_PLUS_PLUS_H__ \ No newline at end of file