Pith - wedge
#include "WedgeCommands.h"
#include <QDate>
#include <QMap>
#include <QRegularExpression>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextEdit>
#include <QtGlobal>

namespace WedgeCommands {

	namespace {

// sequential eval
		bool applyOps(double seed, const QString &tail, double &res) {
			static const QRegularExpression splitRegex("[\\+\\-\\*\\/=]");
			QString ops;
			for (const QChar ch : tail)
if (QStringLiteral("+-*/").contains(ch)) ops += ch;
				const QStringList terms = tail.split(splitRegex, Qt::SkipEmptyParts);
			if (terms.size() != ops.size()) return false;
			res = seed;
			for (int i = 0; i < ops.size(); ++i) {
				const QChar op = ops.at(i);
				QString term = terms.at(i);
				const bool pct = term.endsWith('%');
				if (pct) term.chop(1);
				bool ok = false;
				const double num = term.toDouble(&ok);
				if (!ok) return false;
				const double next = pct
				? ((op == '+' || op == '-') ? res * (num / 100.0) : num / 100.0)
				: num;
				switch (op.unicode()) {
				case '+': res += next; break;
				case '-': res -= next; break;
				case '*': res *= next; break;
				default:  res /= next; break;
				}
			}
			return true;
		}

		bool evalSeq(const QString &expr, double &res) {
			int i = 0;
while (i < expr.size() && !QStringLiteral("+-*/=").contains(expr.at(i))) ++i;
			bool ok = false;
			const double head = expr.left(i).toDouble(&ok);
			return ok && applyOps(head, expr.mid(i), res);
		}

		QString fmtNum(double v) {
			QString s = QString::number(v, 'f', 2);
			if (s.contains('.')) {
				while (s.endsWith('0')) s.chop(1);
				if (s.endsWith('.')) s.chop(1);
			}
			return s;
		}

	}

	QString otcDateResult(int day, QString mon, int year) {
		static QMap<QString, int> months = {
			{"jan", 1}, {"feb", 2}, {"mar", 3}, {"apr", 4}, {"may", 5}, {"jun", 6},
			{"jul", 7}, {"aug", 8}, {"sep", 9}, {"oct", 10}, {"nov", 11}, {"dec", 12}
		};
		int m = months.value(mon.toLower(), 0);
		if (m == 0) return "";
		QDate now = QDate::currentDate();
		QDate target(year > 0 ? year : now.year(), m, day);
		if (!target.isValid()) return "";
		if (year <= 0 && target < now) target = target.addYears(1);
		QDate otcStart(target.year(), 3, 20);
		if (target < otcStart) otcStart = otcStart.addYears(-1);
		return QString("(%1)=%2").arg(otcStart.daysTo(target) + 1).arg(qMax(qint64(0), now.daysTo(target)));
	}

	Result apply(QTextEdit *editor, int key) {
		Result r;
		if (!editor) return r;

		QTextCursor cursor = editor->textCursor();
		QString lineText = cursor.block().text().left(cursor.positionInBlock());

		static QRegularExpression mathRegex("(?<=\\s|^|=)((\\d+(\\.\\d+)?%?[\\+\\-\\*\\/])+\\d+(\\.\\d+)?%?)=$");
		static QRegularExpression dateRegex("(?<=\\s|^)(\\d{1,2})([a-zA-Z]{3})(\\d{2})?\\(\\)=$");
		static QRegularExpression listRegex("^(\\s*)(\\d+)\\.\\s");
		static QRegularExpression hrRegex("(?<=\\s|^)hr\\((\\d+)\\)$");
		static QRegularExpression dashRegex("(?<=\\s|^)dash\\((\\d+)\\)$");

		auto mMatch = mathRegex.match(lineText);
		if (mMatch.hasMatch()) {
			double head = 0;
			if (evalSeq(mMatch.captured(1), head)) {
				const QString resStr = fmtNum(head);

// chain recalc
				static QRegularExpression chainRegex("^((?:[\\+\\-\\*\\/]\\d+(?:\\.\\d+)?%?)+)=(-?\\d+(?:\\.\\d+)?)?");
				const QString rest = cursor.block().text().mid(cursor.positionInBlock());
				double acc = resStr.toDouble();
				QString tail;
				int used = 0;
				for (;;) {
					const QRegularExpressionMatch c = chainRegex.match(rest.mid(used));
					if (!c.hasMatch()) break;
					double v = 0;
					if (!applyOps(acc, c.captured(1), v)) break;
					const QString shown = fmtNum(v);
					acc = shown.toDouble();
					tail += c.captured(1) + "=" + shown;
					used += c.capturedLength();
				}

				editor->blockSignals(true);
				cursor.beginEditBlock();
				cursor.insertText(resStr);
				if (used > 0) {
					QTextCursor t(cursor);
					t.setPosition(cursor.position() + used, QTextCursor::KeepAnchor);
					t.insertText(tail);
				}
				cursor.endEditBlock();
				editor->blockSignals(false);
				r.changed = true;
			}
			r.consumed = (key == Qt::Key_Space);
			return r;
		}

		auto dMatch = dateRegex.match(lineText);
		if (dMatch.hasMatch()) {
			const int year = dMatch.captured(3).isEmpty() ? -1 : 2000 + dMatch.captured(3).toInt();
			QString res = otcDateResult(dMatch.captured(1).toInt(), dMatch.captured(2), year);
			if (!res.isEmpty()) {
				editor->blockSignals(true);
				cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 3);
				cursor.insertText(res);
				editor->blockSignals(false);
				r.changed = true;
			}
			r.consumed = (key == Qt::Key_Space);
			return r;
		}

		auto hMatch = hrRegex.match(lineText);
		if (hMatch.hasMatch()) {
			int count = hMatch.captured(1).toInt();
			if (count > 0) {
				QString hrline = QString("\u2014").repeated(qMin(count, 100));
				editor->blockSignals(true);
				cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hMatch.capturedLength());
				cursor.insertText(hrline);
				editor->blockSignals(false);
				r.changed = true;
			}
			r.consumed = (key == Qt::Key_Space);
			return r;
		}

		auto emMatch = dashRegex.match(lineText);
		if (emMatch.hasMatch()) {
			int counts = emMatch.captured(1).toInt();
			if (counts > 0) {
				QString dashes = QString("\u2014 ").repeated(qMin(counts, 100));
				editor->blockSignals(true);
				cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, emMatch.capturedLength());
				cursor.insertText(dashes);
				editor->blockSignals(false);
				r.changed = true;
			}
			r.consumed = (key == Qt::Key_Space);
			return r;
		}

		if (key != Qt::Key_Space) {
			auto lMatch = listRegex.match(lineText);
			if (lMatch.hasMatch()) {
				QString ws = lMatch.captured(1);
				int num = lMatch.captured(2).toInt();
				QString prefix = "\n" + ws + QString::number(num + 1) + ". ";
				editor->blockSignals(true);
				cursor.beginEditBlock();
				cursor.insertText(prefix);
				QTextBlock nextBlock = cursor.block().next();
				int nextExpected = num + 1;
				int i = 0;
				while (nextBlock.isValid()) {
					auto m = listRegex.match(nextBlock.text());
					if (m.hasMatch() && m.captured(1) == ws && m.captured(2).toInt() == nextExpected) {
						int foundNum = m.captured(2).toInt();
						QTextCursor nextCursor(nextBlock);
						nextCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m.capturedLength());
						nextCursor.insertText(m.captured(1) + QString::number(foundNum + 1) + ". ");
						nextExpected++;
					} else if (i > 0) break;
					nextBlock = nextBlock.next(); i++;
				}
				cursor.endEditBlock();
				editor->blockSignals(false);
				r.changed = true;
				r.consumed = true;
				return r;
			}
			static QRegularExpression bulletRegex("^([ \\t]*)(-) ");
			auto bMatch = bulletRegex.match(lineText);
			if (bMatch.hasMatch()) {
				editor->blockSignals(true);
				cursor.insertText("\n" + bMatch.captured(1) + "- ");
				editor->blockSignals(false);
				r.changed = true;
				r.consumed = true;
				return r;
			}
			static QRegularExpression indentRegex("^(\\s+)");
			auto iMatch = indentRegex.match(lineText);
			if (iMatch.hasMatch()) {
				QString indent = "\n" + iMatch.captured(1);
				editor->blockSignals(true);
				cursor.insertText(indent);
				editor->blockSignals(false);
				r.changed = true;
				r.consumed = true;
				return r;
			}
		}
		return r;
	}

}