receta
Compare

Migration Guide

Step-by-step guide to refactoring existing sort code to use Compare module.

Why Migrate?

Before (Array.sort callback):

users.sort((a, b) => {
  if (a.department < b.department) return -1
  if (a.department > b.department) return 1
  if (a.salary > b.salary) return -1
  if (a.salary < b.salary) return 1
  return a.name.localeCompare(b.name)
})

After (Compare module):

import { compose, ascending, descending, caseInsensitive } from '@ontologyio/receta/compare'

users.sort(
  compose(
    ascending(u => u.department),
    descending(u => u.salary),
    caseInsensitive(u => u.name)
  )
)

✅ More readable
✅ Type-safe
✅ Reusable
✅ Testable
✅ Composable

Step 1: Identify Sortable Fields

Find all Array.sort() calls in your codebase:

# Search for sort callbacks
grep -r "\.sort\((a, b)" src/
grep -r "\.sort\(function" src/

Document each sort's logic:

  • What fields are being compared?
  • What's the sort order (asc/desc)?
  • Are nulls handled?
  • Is it multi-level?

Step 2: Replace Simple Sorts

Pattern 1: Single Field Ascending

// Before
numbers.sort((a, b) => a - b)

// After
import { ascending } from '@ontologyio/receta/compare'
numbers.sort(ascending(x => x))

Pattern 2: Single Field Descending

// Before
scores.sort((a, b) => b - a)

// After
import { descending } from '@ontologyio/receta/compare'
scores.sort(descending(x => x))

Pattern 3: Object Property

const users = [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]

// Before
users.sort((a, b) => a.age - b.age)

// After
import { ascending } from '@ontologyio/receta/compare'
users.sort(ascending(u => u.age))

// Or use byKey for direct property access
import { byKey } from '@ontologyio/receta/compare'
users.sort(byKey('age'))

Pattern 4: String Comparison

// Before
names.sort((a, b) => a.localeCompare(b))

// After
import { ascending } from '@ontologyio/receta/compare'
names.sort(ascending(x => x))

Pattern 5: Date Comparison

// Before
events.sort((a, b) => a.date.getTime() - b.date.getTime())

// After
import { byDate } from '@ontologyio/receta/compare'
events.sort(byDate(e => e.date))

Step 3: Replace Multi-Level Sorts

Pattern: Nested If Statements

// Before
items.sort((a, b) => {
  if (a.priority !== b.priority) {
    return b.priority - a.priority // Higher priority first
  }
  if (a.status !== b.status) {
    return a.status.localeCompare(b.status)
  }
  return a.createdAt.getTime() - b.createdAt.getTime()
})

// After
import { compose, descending, ascending, byDate } from '@ontologyio/receta/compare'

items.sort(
  compose(
    descending(i => i.priority),
    ascending(i => i.status),
    byDate(i => i.createdAt)
  )
)

Pattern: Ternary Chain

// Before
products.sort((a, b) =>
  a.category !== b.category
    ? a.category.localeCompare(b.category)
    : b.rating !== a.rating
    ? b.rating - a.rating
    : a.price - b.price
)

// After
import { compose, ascending, descending } from '@ontologyio/receta/compare'

products.sort(
  compose(
    ascending(p => p.category),
    descending(p => p.rating),
    ascending(p => p.price)
  )
)

Step 4: Handle Null Values

Pattern: Nullish Coalescing

// Before
users.sort((a, b) => {
  const aVal = a.lastLogin ?? new Date(0)
  const bVal = b.lastLogin ?? new Date(0)
  return aVal.getTime() - bVal.getTime()
})

// After
import { ascending } from '@ontologyio/receta/compare'

users.sort(ascending(u => u.lastLogin ?? new Date(0)))

Pattern: Explicit Null Checks

// Before
items.sort((a, b) => {
  if (a.value === null && b.value === null) return 0
  if (a.value === null) return -1
  if (b.value === null) return 1
  return a.value - b.value
})

// After
import { ascending, byBoolean, compose } from '@ontologyio/receta/compare'

items.sort(
  compose(
    byBoolean(i => i.value !== null), // false sorts first, so nulls lead
    ascending(i => i.value ?? 0)
  )
)

Step 5: Handle String Case

Pattern: toLowerCase() Comparison

// Before
files.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))

// After
import { caseInsensitive } from '@ontologyio/receta/compare'

files.sort(caseInsensitive(f => f.name))

Pattern: Case-Sensitive with Locale

// Before
const collator = new Intl.Collator('de-DE')
names.sort((a, b) => collator.compare(a, b))

// After
import { localeCompare } from '@ontologyio/receta/compare'

names.sort(localeCompare(x => x, 'de-DE'))

Step 6: Handle Natural Sorting

Pattern: Numeric String Comparison

// Before (wrong - lexicographic)
files.sort((a, b) => a.name.localeCompare(b.name))
// => ['file1.txt', 'file10.txt', 'file2.txt'] ❌

// After
import { natural } from '@ontologyio/receta/compare'

files.sort(natural(f => f.name))
// => ['file1.txt', 'file2.txt', 'file10.txt'] ✅

Step 7: Extract Reusable Comparators

Before:

// Repeated in multiple places
function sortUsersByName(users: User[]) {
  return users.sort((a, b) => a.lastName.localeCompare(b.lastName))
}

function sortEmployeesByName(employees: Employee[]) {
  return employees.sort((a, b) => a.lastName.localeCompare(b.lastName))
}

After:

import { ascending } from '@ontologyio/receta/compare'

// Reusable comparator
const byLastName = ascending((person: { lastName: string }) => person.lastName)

// Use everywhere
function sortUsersByName(users: User[]) {
  return users.sort(byLastName)
}

function sortEmployeesByName(employees: Employee[]) {
  return employees.sort(byLastName)
}

Step 8: Parameterize Sort Direction

Before:

function sortProducts(products: Product[], direction: 'asc' | 'desc') {
  return products.sort((a, b) => {
    const diff = a.price - b.price
    return direction === 'asc' ? diff : -diff
  })
}

After:

import { ascending, descending } from '@ontologyio/receta/compare'

function sortProducts(products: Product[], direction: 'asc' | 'desc') {
  const comparator = direction === 'asc'
    ? ascending(p => p.price)
    : descending(p => p.price)

  return products.sort(comparator)
}

Migration Checklist

For each Array.sort() in your codebase:

  • Identify all fields being compared
  • Determine sort order for each field (asc/desc)
  • Check for null/undefined handling
  • Look for string case handling
  • Check if natural sorting is needed
  • Identify if it's single or multi-level
  • Replace with appropriate Compare functions
  • Test that sort order is identical
  • Extract reusable comparators if repeated
  • Update tests to use new comparators

Common Pitfalls

Pitfall 1: Forgetting to Return from Arrow Functions

// ❌ Wrong
const comparator = ascending(x => { x.value })

// ✅ Correct
const comparator = ascending(x => x.value)

Pitfall 2: Mutating During Comparison

// ❌ Don't mutate in comparator
items.sort(ascending((x: Item) => {
  x.viewed = true  // Side effect!
  return x.priority
}))

// ✅ Pure extraction
items.sort(ascending((x: Item) => x.priority))

Pitfall 3: Wrong compose() Order

// ❌ Wrong order (priority before status)
compose(
  ascending((x: Item) => x.priority),
  ascending((x: Item) => x.status)
)

// ✅ Correct (status before priority)
compose(
  ascending((x: Item) => x.status),
  ascending((x: Item) => x.priority)
)

// compose() applies in order: first has highest priority

Pitfall 4: Comparing Different Types

// ❌ Mixing types
ascending((x: Item) => x.date ?? 'never')  // Date | string

// ✅ Consistent types
ascending((x: Item) => x.date ?? new Date(0))  // Always Date

Testing Your Migration

Unit Test Template

import { describe, it, expect } from 'bun:test'
import { yourComparator } from './comparators'

describe('Migration: Product sorting', () => {
  it('maintains same order as original implementation', () => {
    const products = [{ price: 20 }, { price: 10 }]

    // Original implementation
    const originalSort = [...products].sort((a, b) => a.price - b.price)

    // New implementation
    const newSort = [...products].sort(yourComparator)

    expect(newSort).toEqual(originalSort)
  })

  it('handles edge cases', () => {
    const edgeCases = [{ price: 0 }, { price: 10 }, { price: 10 }]
    const sorted = edgeCases.sort(yourComparator)
    expect(sorted).toBeDefined()
  })
})

Incremental Migration Strategy

  1. Start with leaf nodes - Migrate simplest sorts first
  2. One file at a time - Don't refactor everything at once
  3. Test thoroughly - Ensure sort order is identical
  4. Extract patterns - Create reusable comparators
  5. Document decisions - Note any behavioral changes

Next Steps

On this page