Latest posts

Starting on my own

After years at Unfold, I'm going solo. I'm freelancing and building my own product, empowered by AI to do alone what used to require a team.

Exciting times! Reach out if you need help with a project.

Back in action

It's been a while since my last post. Actually, it's been several years. But I'm back in action!

A few significant things have changed in the meantime. Most importantly, I've changed my name from Hampus Nilsson to Hampus Borgos (after marrying my wife, who runs borgosdesigns.no).

Along with the name change, I've moved this blog to a new domain. My old domain, hjnilsson.com, is unfortunately no longer mine and is now parked by someone else. You can now find my blog and portfolio at kodeverk.com.

I'm looking forward to getting back into the habit of sharing my thoughts and projects here. Stay tuned!

A simple math parser

Here is a simple math expression parser for TypeScript.

It can take an input string like "2+2\*4" and properly return 10 as the result. This can be used as a quick and lightweight way to do math on user-input expressions.

export type ParseResult = number | '' | Error

type Token =
    | { type: 'number'; value: number }
    | { type: 'op'; value: '+' | '-' | '*' | '/' }
    | { type: 'lparen' }
    | { type: 'rparen' }

function isDigit(char: string): boolean {
    return char >= '0' && char <= '9'
}

function normalizeDecimals(input: string): string {
    // Replace commas with dots and remove spaces
    return input.replace(/,/g, '.').replace(/[\s]/g, '')
}

function tokenize(input: string): Token[] | Error {
    const src = normalizeDecimals(input)
    const tokens: Token[] = []
    let i = 0

    while (i < src.length) {
        const ch = src[i]

        if (isDigit(ch) || ch === '.') {
            const start = i
            let seenDot = ch === '.'
            i++
            while (i < src.length) {
                const c = src[i]
                if (isDigit(c)) {
                    i++
                } else if (c === '.') {
                    if (seenDot) break
                    seenDot = true
                    i++
                } else {
                    break
                }
            }
            const numStr = src.slice(start, i)
            if (numStr === '.' || numStr === '')
                return new Error('Invalid number')
            const value = Number(numStr)
            if (Number.isNaN(value)) return new Error('Invalid number')
            tokens.push({ type: 'number', value })
            continue
        }

        if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
            tokens.push({ type: 'op', value: ch })
            i++
            continue
        }

        if (ch === '(') {
            tokens.push({ type: 'lparen' })
            i++
            continue
        }
        if (ch === ')') {
            tokens.push({ type: 'rparen' })
            i++
            continue
        }

        return new Error('Invalid character')
    }

    return tokens
}

// Parser using recursive descent with support for unary +/-
// Grammar (after tokenization and whitespace removal):
// Expression := Term ((+|-) Term)*
// Term       := Factor ((*|/) Factor)*
// Factor     := (+|-) Factor | Primary
// Primary    := Number | ( Expression )

function parseAndEvaluate(tokens: Token[]): number | Error {
    let pos = 0

    function peek(): Token | undefined {
        return tokens[pos]
    }

    function consume<T extends Token['type']>(
        type: T,
    ): Extract<Token, { type: T }> | undefined {
        if (tokens[pos]?.type === type) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            return tokens[pos++] as any
        }
        return undefined
    }

    function parseExpression(): number | Error {
        let left = parseTerm()
        if (left instanceof Error) return left

        // eslint-disable-next-line no-constant-condition
        while (true) {
            const t = peek()
            if (t && t.type === 'op' && (t.value === '+' || t.value === '-')) {
                pos++
                const right = parseTerm()
                if (right instanceof Error) return right
                left = t.value === '+' ? left + right : left - right
            } else {
                break
            }
        }
        return left
    }

    function parseTerm(): number | Error {
        let left = parseFactor()
        if (left instanceof Error) return left

        // eslint-disable-next-line no-constant-condition
        while (true) {
            const t = peek()
            if (t && t.type === 'op' && (t.value === '*' || t.value === '/')) {
                pos++
                const right = parseFactor()
                if (right instanceof Error) return right
                if (t.value === '/') {
                    if (right === 0) {
                        // Per requirement: return 0 on division by zero
                        left = 0
                    } else {
                        left = left / right
                    }
                } else {
                    left = left * right
                }
            } else {
                break
            }
        }
        return left
    }

    function parseFactor(): number | Error {
        const t = peek()
        if (!t) return new Error('Unexpected end')

        // Unary operators
        if (t.type === 'op' && (t.value === '+' || t.value === '-')) {
            pos++
            const val = parseFactor()
            if (val instanceof Error) return val
            return t.value === '+' ? +val : -val
        }

        return parsePrimary()
    }

    function parsePrimary(): number | Error {
        const t = peek()
        if (!t) return new Error('Unexpected end')
        if (t.type === 'number') {
            pos++
            return t.value
        }
        if (t.type === 'lparen') {
            pos++
            const value = parseExpression()
            if (value instanceof Error) return value
            const r = consume('rparen')
            if (!r) return new Error('Unbalanced parentheses')
            return value
        }
        return new Error('Unexpected token')
    }

    const result = parseExpression()
    if (result instanceof Error) return result
    if (pos !== tokens.length) return new Error('Trailing input')
    return result
}

export function parseMath(input: string): ParseResult {
    const trimmed = input.trim()
    if (trimmed === '') return ''

    const tokens = tokenize(trimmed)
    if (tokens instanceof Error) return tokens

    // Disallow expressions that end with an operator
    if (tokens.length > 0) {
        const last = tokens[tokens.length - 1]
        if (last.type === 'op') return new Error('Ends with operator')
        // Leading illegal operator: '*' or '/'
        const first = tokens[0]
        if (
            first.type === 'op' &&
            (first.value === '*' || first.value === '/')
        ) {
            return new Error('Illegal leading operator')
        }
    }

    const value = parseAndEvaluate(tokens)
    return value
}

I've used this as a common enhancement in projects I've been working on, for number input fields. The luxury of not leaving the application to do a calculation (especially on mobile) is both a great and a subtle improvement.

Mocking the Apollo Query provider

When working with React components that use Apollo Client, you often need to mock the Apollo Provider for testing or Storybook. This allows you to test your components in isolation without setting up a full GraphQL server or making actual network requests.

The challenge is that Apollo's ApolloProvider expects a full Apollo Client instance, which can be complex to mock. However, for most testing scenarios, you don't need the full client—you just need to mock the query methods that your components use.

Here's a simple solution that creates a mock Apollo Provider that accepts predefined query results. This is perfect for:

  • Storybook: Show your components with different data states without a backend
  • Unit tests: Test component rendering and behavior with controlled data
  • Integration tests: Verify component interactions without network calls

The MockApolloQuery component wraps your components and provides mock query results based on the query name. It handles both readQuery and watchQuery methods, which covers most common Apollo usage patterns.

import { ApolloProvider } from 'react-apollo'
import React from 'react'

function resultForQuery(
  ctx: any,
  results: { [queryName: string]: { data?: any; loading?: boolean } },
) {
  const query = ctx.query.definitions.find(
    (op: any) => op.kind === 'OperationDefinition' && Object.keys(results).includes(op.name.value),
  )

  if (query) {
    return {
      subscribe: () => ({
        query: 'dummy-MockApolloQuery',
      }),
      resetQueryStoreErrors: () => {},
      options: {
        fetchPolicy: 'cache-only',
      },
      setOptions: async () => new Promise(resolve => setTimeout(resolve, 0)),
      getCurrentResult: () => {
        return {
          data: results[query.name.value].data,
          loading: results[query.name.value].loading ?? false,
        }
      },
    }
  }

  throw new Error(
    `Called with unknown query ${ctx.query.definitions
      .filter((op: any) => op.kind === 'OperationDefinition')
      .map((op: any) => op.name.value)}, must mock all queries for MockApolloQuery to work.`,
  )
}

export function MockApolloQuery({
  results,
  children,
}: {
  results: { [queryName: string]: { data?: any; loading?: boolean } }
  children: React.ReactNode
}) {
  return (
    <ApolloProvider
      client={
        {
          readQuery: (ctx: any) => resultForQuery(ctx, results),
          watchQuery: (ctx: any) => resultForQuery(ctx, results),
        } as any
      }
    >
      {children}
    </ApolloProvider>
  )
}

It is the best.

Tracking the Swedish Election: val.digital - JavaZone

My talk at JavaZone this year on how to make your own hobby projects succesful and how to stay succesful. It was well received at the conference, and hope it you feel the same! My hope is that people will take what they know, make simple things and move from there.

Video of the talk at Vimeo.
View ArchiveOlder posts →