While working on a collection of browser-based developer tools, I kept running into the same problem: I needed to balance chemical equations for a chemistry study tool, and every existing solution was either a heavy external API call or a clunky desktop app. I wanted something that worked entirely in the browser — no server, no dependencies, just pure math and JavaScript.
The result was a small single-file tool that solves this with linear algebra. Here's how I approached it, including the parts where my AI assistant confidently led me astray.
The Problem: Balancing Equations Is Linear Algebra in Disguise
Balancing a chemical equation like H2 + O2 = H2O is essentially solving a system of linear equations. Each element gives you one equation, and you're solving for the coefficients that make atoms conserved on both sides.
For H2 + O2 = H2O:
- Hydrogen: 2a = 2c
- Oxygen: 2b = c
Where a, b, and c are the coefficients for H2, O2, and H2O respectively.
The trick is that this is an underdetermined system — you have more variables than equations. The solution is a vector in the null space of the coefficient matrix. Find that vector, scale it to integers, and you're done.
Parsing Chemical Formulas: The Part That Made Me Question My Life Choices
The math is elegant. The parsing is where things get ugly.
Chemical formulas have nested parentheses: Al2(SO4)3 means 2 Al, 3 S, and 12 O. I needed a parser that could handle this recursively.
My initial approach was a simple regex-based parser. That worked for flat formulas like H2O but completely broke on anything with parentheses.
The better approach: a recursive descent parser that processes the formula character by character.
function parseFormula(formula) {
const counts = {};
let i = 0;
function parseGroup(multiplier = 1) {
let localCounts = {};
while (i < formula.length) {
if (formula[i] === '(') {
i++;
const subCounts = parseGroup();
// Handle subscript after closing paren
let subMult = 1;
if (/\d/.test(formula[i])) {
subMult = parseInt(formula[i]);
i++;
}
for (const [elem, count] of Object.entries(subCounts)) {
localCounts[elem] = (localCounts[elem] || 0) + count * subMult;
}
} else if (formula[i] === ')') {
i++;
return localCounts;
} else if (/[A-Z]/.test(formula[i])) {
let elem = formula[i++];
if (/[a-z]/.test(formula[i])) {
elem += formula[i++];
}
let count = 1;
if (/\d/.test(formula[i])) {
count = parseInt(formula[i]);
i++;
}
localCounts[elem] = (localCounts[elem] || 0) + count;
}
}
return localCounts;
}
const result = parseGroup();
// Apply top-level multiplier if needed
return result;
}
The key insight: handle the (SO4)3 pattern by recursively parsing inside parentheses, then multiplying all counts by the subscript. This small function handles the recursive structure elegantly.
Gaussian Elimination: Where the AI Saved My Sanity
The linear algebra part is where most implementations fail. Here's the core idea:
- Build a matrix where rows are elements and columns are compounds
- Reactants get positive coefficients, products get negative
- Find the null space vector using Gaussian elimination
- Scale to smallest integer coefficients
The AI assistant was actually quite good at this part. It generated a solid Gaussian elimination implementation on the first try. The issue was everything around it.
The AI Collaboration: A Story of Confident Mistakes
Here's where I need to be honest about the AI-assisted development experience. I'm using this process for all my tools now, and it's a mixed bag.
What the AI did well:
- The Gaussian elimination algorithm was correct on the first attempt
- The basic structure was solid
- The i18n setup was clean
What the AI got wrong:
- It initially parsed formulas incorrectly for compounds with multiple capital letters like
NaCl— it treatedNaasNfollowed bya - It didn't handle the case where coefficients could be zero (which means an element appears on only one side)
- The UI was ugly — I had to add proper styling and dark mode support myself
The parsing bug was particularly frustrating. The AI kept assuming element symbols are single letters followed by optional lowercase. That works for H2O but breaks for NaCl or CaCO3.
I had to explicitly show it the pattern for multi-letter element symbols:
// Wrong: /[A-Z][a-z]?/ - only handles one lowercase letter
// Right: /[A-Z][a-z]*/ - handles Na, Ca, Mg, etc.
The "Works on My Machine" Moment
The first version worked perfectly for all my test cases. I was ready to ship it.
Then I tried Fe + O2 = Fe2O3.
The AI's implementation gave me 4Fe + 3O2 = 2Fe2O3 — which is correct, but the intermediate steps were wrong. The matrix had a zero row that wasn't being handled properly, and the AI's "solution" was to add a special case.
The real fix was understanding that the null space computation needs to handle rank deficiency properly. When you have more compounds than elements, there's always a non-zero solution. But when you have fewer, you might get a trivial solution.
Performance and Edge Cases
The algorithm runs in O(n³) where n is the number of compounds. For typical chemistry equations (up to 10 compounds), this is instantaneous. No performance concerns.
But there are edge cases that will break any implementation:
-
H2O = H2O(already balanced) — should return coefficients of 1 -
C6H12O6 = C6H12O6(same compound on both sides) — trivial solution - Equations with fractional coefficients that can't be scaled to integers
The last one is interesting. Some equations legitimately have fractional solutions, but for chemistry, we want integers. The AI initially just returned the fractional solution without trying to scale it.
The Final Architecture
The tool ended up being a single HTML file with three main components:
- Parser: Converts chemical formulas to element counts
- Solver: Uses Gaussian elimination to find minimal integer coefficients
- Renderer: Displays the balanced equation with proper subscripts and a conservation table
The conservation table was a nice touch — it shows that hydrogen starts with 4 atoms and ends with 4 atoms, so you can visually verify the balancing worked.
Lessons Learned
On AI-assisted development: The AI is a powerful pair programmer, but it's not a replacement for understanding the problem. I had to debug the parser logic myself because the AI kept making the same mistake. It's great for generating boilerplate and standard algorithms, but domain-specific logic still requires human oversight.
On the math: Linear algebra is everywhere in programming. This project was a reminder that understanding the math behind the problem leads to cleaner solutions than brute-force approaches.
On chemical formulas: Parsing is always harder than it looks. The recursive descent approach handles all the edge cases but requires careful implementation.
The Result
During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles parsing, balancing, and verification entirely in the browser. No API calls, no server-side processing, just math and JavaScript.
The tool is part of my collection of browser-based utilities at Craftvo. If you're working with chemistry or just want to see the implementation, it's all vanilla JavaScript — feel free to peek at the source.
The next time you need to balance an equation, remember: it's just linear algebra wearing a chemistry costume. And like most things in programming, the parser is the part that'll make you question your career choices.
Tags: javascript, chemistry, algorithms, linear-algebra, webdev
Top comments (0)