<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Monsibd</title>
    <description>The latest articles on DEV Community by Monsibd (@monsibd).</description>
    <link>https://dev.to/monsibd</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158643%2Fb407a0e2-5f60-4abc-9751-8056abc2c3f3.gif</url>
      <title>DEV Community: Monsibd</title>
      <link>https://dev.to/monsibd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/monsibd"/>
    <language>en</language>
    <item>
      <title>Turning "price - product cost - shipping" into a calculator: a plain-word formula parser with no eval</title>
      <dc:creator>Monsibd</dc:creator>
      <pubDate>Fri, 09 Oct 2026 03:11:41 +0000</pubDate>
      <link>https://dev.to/monsibd/turning-price-product-cost-shipping-into-a-calculator-a-plain-word-formula-parser-with-no-11pl</link>
      <guid>https://dev.to/monsibd/turning-price-product-cost-shipping-into-a-calculator-a-plain-word-formula-parser-with-no-11pl</guid>
      <description>&lt;p&gt;Most people don't think in spreadsheet syntax. Ask an online seller how they work out profit per order and they'll say something like "price minus product cost minus shipping." Not &lt;code&gt;=B2-C2-D2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wanted a calculator builder where you can type exactly that, &lt;code&gt;price - product cost - shipping&lt;/code&gt;, and get a working calculator with labelled inputs. Here's how the parser behind it works, in about 100 lines of plain JavaScript and with no &lt;code&gt;eval&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: tidy the formula without changing its meaning
&lt;/h2&gt;

&lt;p&gt;People type formulas the way they think: &lt;code&gt;=Price x 2&lt;/code&gt;, &lt;code&gt;fee ÷ 100&lt;/code&gt;, &lt;code&gt;revenue – costs&lt;/code&gt; (with an en dash). So before parsing anything, a &lt;code&gt;tidyFormula&lt;/code&gt; pass normalizes the typography:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tidyFormula&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*=&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// drop a leading "="&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;×✕✖&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/÷/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;−–—&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\^&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;([\w&lt;/span&gt;&lt;span class="sr"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\]])\s&lt;/span&gt;&lt;span class="sr"&gt;+x&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?=[\w&lt;/span&gt;&lt;span class="sr"&gt;(&lt;/span&gt;&lt;span class="se"&gt;])&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$1 * &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// " x " means multiply&lt;/span&gt;
  &lt;span class="c1"&gt;// ...then join multi-word names (next step)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;x&lt;/code&gt; rule only fires between two operands, so a variable called &lt;code&gt;x&lt;/code&gt; on its own still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: multi-word names become one variable
&lt;/h2&gt;

&lt;p&gt;This is the part that makes plain words work. Any run of words separated only by spaces becomes a single snake_case name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;price - product cost - shipping   -&amp;gt;   price - product_cost - shipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex walks the string and grabs &lt;code&gt;[A-Za-z_][A-Za-z0-9_]*&lt;/code&gt; followed by more space-separated words. Two edge cases matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Numbers like &lt;code&gt;1e6&lt;/code&gt;.&lt;/strong&gt; If the character before a match is a digit or a dot, it's part of a number, not a name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function calls.&lt;/strong&gt; In &lt;code&gt;selling price round(...)&lt;/code&gt; you don't want &lt;code&gt;selling_price_round&lt;/code&gt;. If the last word is a known function and it's followed by &lt;code&gt;(&lt;/code&gt;, it's kept separate.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: detect the inputs, in order
&lt;/h2&gt;

&lt;p&gt;Once the formula is tidy, every remaining identifier that isn't a function name (&lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;abs&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt;, &lt;code&gt;floor&lt;/code&gt;, &lt;code&gt;ceil&lt;/code&gt;) or a constant (&lt;code&gt;e&lt;/code&gt;, &lt;code&gt;pi&lt;/code&gt;) is an input. They're collected in the order they first appear, so the form fields come out in the same order you wrote them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: turn keys back into human labels
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;product_cost&lt;/code&gt; becomes "Product cost". A small dictionary handles the abbreviations people actually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LABEL_WORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;quantity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COGS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ROAS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mrr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MRR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cac&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ltv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LTV&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;aov&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AOV&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;year&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;week&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// "fee_pct" -&amp;gt; "Fee %", "units_sold" -&amp;gt; "Units sold"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also guesses how the answer should read: if the formula ends in &lt;code&gt;* 100&lt;/code&gt; it's a percentage, and if any input looks like money (&lt;code&gt;price&lt;/code&gt;, &lt;code&gt;cost&lt;/code&gt;, &lt;code&gt;revenue&lt;/code&gt;, &lt;code&gt;fee&lt;/code&gt;...) the result is formatted as currency. It's a guess you can override, but it's right most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: evaluate safely, without eval
&lt;/h2&gt;

&lt;p&gt;The site's Content Security Policy doesn't allow &lt;code&gt;unsafe-eval&lt;/code&gt;, so &lt;code&gt;new Function("return " + expr)&lt;/code&gt; was never an option. Instead there's a small recursive-descent evaluator that supports exactly what business formulas need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;numbers (including &lt;code&gt;1e6&lt;/code&gt;), &lt;code&gt;+ - * /&lt;/code&gt;, and &lt;code&gt;**&lt;/code&gt; (right-associative)&lt;/li&gt;
&lt;li&gt;parentheses and unary &lt;code&gt;+&lt;/code&gt;/&lt;code&gt;-&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;comparisons &lt;code&gt;&amp;lt; &amp;gt; &amp;lt;= &amp;gt;= == !=&lt;/code&gt;, logical &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; and &lt;code&gt;||&lt;/code&gt; (short-circuit)&lt;/li&gt;
&lt;li&gt;the ternary &lt;code&gt;?:&lt;/code&gt;, so &lt;code&gt;units &amp;gt; 100 ? price * 0.9 : price&lt;/code&gt; gives you volume pricing&lt;/li&gt;
&lt;li&gt;a whitelist of functions with argument-count limits (&lt;code&gt;round(x, 2)&lt;/code&gt; takes one or two arguments, &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt; up to 20)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each grammar level is its own function: ternary, then &lt;code&gt;||&lt;/code&gt;, then &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, then equality, then relational, then &lt;code&gt;+ -&lt;/code&gt;, then &lt;code&gt;* /&lt;/code&gt;, then &lt;code&gt;**&lt;/code&gt;, then unary (&lt;code&gt;-&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;!&lt;/code&gt;), then primary (numbers, parentheses and function calls). Anything unexpected throws an error that names the position, which then becomes a friendly message in the UI.&lt;/p&gt;

&lt;p&gt;One bug worth sharing: numbers of 1e21 or more stringify as &lt;code&gt;1e+21&lt;/code&gt;. An early version scanned the substituted string for leftover identifiers, saw the &lt;code&gt;e&lt;/code&gt;, and zeroed it out, which quietly turned 1e+21 into 21. The fix was to match the full scientific-notation token first and only treat real identifiers as names. That kind of wrong-but-plausible number is the worst kind of calculator bug, because nothing looks broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: chain calculators together
&lt;/h2&gt;

&lt;p&gt;A single answer is rarely the end of the question. Pricing a product means landed cost, then a price, then profit per order. So the second half of the product is a workflow builder: each calculator's result can be passed as an input to a later step, including one from two steps back. For example: 320 customers at $49 is $15,680 MRR, which annualizes to $188,160 ARR, which is $1,128,960 at a 6x multiple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: let an AI assistant run them
&lt;/h2&gt;

&lt;p&gt;The same calculators are exposed through a JSON API and a remote MCP server, with three tools: &lt;code&gt;search_calculators&lt;/code&gt;, &lt;code&gt;describe_calculator&lt;/code&gt; and &lt;code&gt;calculate&lt;/code&gt;. An assistant like Claude can find a calculator by keyword, read its inputs and run it, so you and the assistant get the same number every time instead of it improvising the formula.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The builder is live at &lt;a href="https://monsitools.com/build-tool" rel="noopener noreferrer"&gt;monsitools.com/build-tool&lt;/a&gt;. Type &lt;code&gt;price - product cost - shipping&lt;/code&gt; and watch the fields appear. The workflow builder is at &lt;a href="https://monsitools.com/build-workflow" rel="noopener noreferrer"&gt;monsitools.com/build-workflow&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm building this solo from Dhaka, and I'd love feedback on the parser, especially formulas you'd expect to work that don't.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>parsing</category>
    </item>
  </channel>
</rss>
