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.
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.
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!
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.
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:
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.
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.