Compare
Common Patterns & Recipes
Production-ready sorting patterns for real-world applications.
GitHub Issues Management
import { compose, ascending, descending, byBoolean, byDate } from '@ontologyio/receta/compare'
interface GitHubIssue {
number: number
title: string
status: 'open' | 'closed'
priority: 'low' | 'medium' | 'high'
assignee: string | null
labels: string[]
createdAt: Date
updatedAt: Date
comments: number
}
// Pattern 1: Default issue list (status → priority → age)
const statusOrder = { open: 0, closed: 1 }
const priorityOrder = { high: 0, medium: 1, low: 2 }
export const sortIssuesDefault = compose(
ascending((i: GitHubIssue) => statusOrder[i.status]),
ascending((i: GitHubIssue) => priorityOrder[i.priority]),
byDate((i: GitHubIssue) => i.createdAt)
)
// Pattern 2: Triage view (unassigned → high priority → oldest)
export const sortIssuesTriage = compose(
byBoolean((i: GitHubIssue) => i.assignee !== null),
ascending((i: GitHubIssue) => priorityOrder[i.priority]),
byDate((i: GitHubIssue) => i.createdAt)
)
// Pattern 3: Activity view (most discussed → recently updated)
export const sortIssuesActivity = compose(
descending((i: GitHubIssue) => i.comments),
descending((i: GitHubIssue) => i.updatedAt)
)
// Pattern 4: User-specific (assigned to me → priority → deadline)
export function sortIssuesForUser(username: string) {
return compose(
ascending((i: GitHubIssue) => i.assignee === username ? 0 : 1),
ascending((i: GitHubIssue) => priorityOrder[i.priority]),
byDate((i: GitHubIssue) => i.createdAt)
)
}Stripe Payment Dashboard
interface StripePayment {
id: string
amount: number
currency: string
status: 'succeeded' | 'pending' | 'failed' | 'refunded'
customer: string
created: Date
description: string
}
// Pattern 1: Alert view (failed first → largest → most recent)
const statusPriority = { failed: 0, pending: 1, refunded: 2, succeeded: 3 }
export const sortPaymentsAlerts = compose(
ascending((p: StripePayment) => statusPriority[p.status]),
descending((p: StripePayment) => p.amount),
descending((p: StripePayment) => p.created)
)
// Pattern 2: Financial view (by currency → amount → date)
export const sortPaymentsFinancial = compose(
ascending((p: StripePayment) => p.currency),
descending((p: StripePayment) => p.amount),
descending((p: StripePayment) => p.created)
)
// Pattern 3: Customer view (by customer → most recent)
export const sortPaymentsByCustomer = compose(
ascending((p: StripePayment) => p.customer),
descending((p: StripePayment) => p.created)
)Data Table with Column Sorting
interface Employee {
id: number
name: string
department: string
salary: number
hireDate: Date
isActive: boolean
}
interface SortConfig {
column: keyof Employee
direction: 'asc' | 'desc'
}
export function buildTableComparator(configs: SortConfig[]): Comparator<Employee> {
const comparators = configs.map(({ column, direction }) => {
let base: Comparator<Employee>
switch (column) {
case 'hireDate':
base = byDate(e => e.hireDate)
break
case 'salary':
base = byNumber(e => e.salary)
break
case 'isActive':
base = byBoolean(e => e.isActive)
break
default:
base = caseInsensitive(e => String(e[column]))
}
return direction === 'desc' ? reverse(base) : base
})
return compose(...comparators)
}
// Usage: User clicks "Department ↑, Salary ↓, Name ↑"
const comparator = buildTableComparator([
{ column: 'department', direction: 'asc' },
{ column: 'salary', direction: 'desc' },
{ column: 'name', direction: 'asc' }
])
employees.sort(comparator)File Browser
interface FileEntry {
name: string
type: 'file' | 'directory'
size: number
modifiedAt: Date
extension: string
}
// Pattern 1: Standard view (directories → natural name)
export const sortFilesStandard = compose(
ascending((f: FileEntry) => f.type === 'directory' ? 0 : 1),
natural((f: FileEntry) => f.name)
)
// Pattern 2: By size (largest first, directories last)
export const sortFilesBySize = compose(
ascending((f: FileEntry) => f.type === 'directory' ? 1 : 0),
descending((f: FileEntry) => f.size)
)
// Pattern 3: By modified date (most recent first)
export const sortFilesByDate = descending((f: FileEntry) => f.modifiedAt)
// Pattern 4: By type (group extensions)
export const sortFilesByType = compose(
ascending((f: FileEntry) => f.type === 'directory' ? 0 : 1),
ascending((f: FileEntry) => f.extension),
natural((f: FileEntry) => f.name)
)E-commerce Product Listings
interface Product {
id: string
name: string
category: string
price: number
rating: number
reviewCount: number
inStock: boolean
releaseDate: Date
}
// Pattern 1: Best sellers (in stock → highest rated → most reviews)
export const sortProductsBestSellers = compose(
reverse(byBoolean((p: Product) => p.inStock)),
descending((p: Product) => p.rating),
descending((p: Product) => p.reviewCount)
)
// Pattern 2: New arrivals (most recent → highest rated)
export const sortProductsNewArrivals = compose(
descending((p: Product) => p.releaseDate),
descending((p: Product) => p.rating)
)
// Pattern 3: Price low to high
export const sortProductsPriceLow = compose(
reverse(byBoolean((p: Product) => p.inStock)),
ascending((p: Product) => p.price)
)
// Pattern 4: By category and rating
export const sortProductsCategoryRating = compose(
ascending((p: Product) => p.category),
descending((p: Product) => p.rating),
descending((p: Product) => p.reviewCount)
)Task Management
interface Task {
id: string
title: string
status: 'todo' | 'in_progress' | 'done'
priority: number // 1-5, 5 is highest
dueDate: Date | null
assignee: string | null
createdAt: Date
}
// Pattern 1: My tasks (in progress → high priority → due soon)
const statusOrder = { in_progress: 0, todo: 1, done: 2 }
export const sortTasksMine = compose(
ascending((t: Task) => statusOrder[t.status]),
descending((t: Task) => t.priority),
ascending((t: Task) => t.dueDate ?? new Date(9999, 0)),
ascending((t: Task) => t.createdAt)
)
// Pattern 2: Team view (unassigned → priority → due date)
export const sortTasksTeam = compose(
byBoolean((t: Task) => t.assignee !== null),
descending((t: Task) => t.priority),
ascending((t: Task) => t.dueDate ?? new Date(9999, 0))
)
// Pattern 3: Overdue tasks (past due → highest priority)
export function sortTasksOverdue(now: Date) {
return compose(
ascending((t: Task) => {
if (!t.dueDate) return 1
return t.dueDate < now ? 0 : 1
}),
ascending((t: Task) => t.dueDate ?? new Date(9999, 0)),
descending((t: Task) => t.priority)
)
}Search Results
interface SearchResult {
id: string
title: string
description: string
relevanceScore: number
category: string
publishedAt: Date
viewCount: number
}
// Pattern 1: By relevance (highest → most viewed)
export const sortSearchByRelevance = compose(
descending((r: SearchResult) => r.relevanceScore),
descending((r: SearchResult) => r.viewCount)
)
// Pattern 2: Most recent (newest → relevant)
export const sortSearchByDate = compose(
descending((r: SearchResult) => r.publishedAt),
descending((r: SearchResult) => r.relevanceScore)
)
// Pattern 3: By category and relevance
export const sortSearchByCategory = compose(
ascending((r: SearchResult) => r.category),
descending((r: SearchResult) => r.relevanceScore)
)
// Pattern 4: Popularity (most viewed → relevant → recent)
export const sortSearchByPopularity = compose(
descending((r: SearchResult) => r.viewCount),
descending((r: SearchResult) => r.relevanceScore),
descending((r: SearchResult) => r.publishedAt)
)Analytics Dashboard
interface Metric {
name: string
current: number
previous: number
change: number
changePercent: number
trend: 'up' | 'down' | 'stable'
}
// Pattern 1: Biggest movers (largest % change)
export const sortMetricsByChange = descending(
(m: Metric) => Math.abs(m.changePercent)
)
// Pattern 2: Alerts (negative trends → largest drops)
export const sortMetricsAlerts = compose(
ascending((m: Metric) => m.trend === 'down' ? 0 : 1),
ascending((m: Metric) => m.change)
)
// Pattern 3: Performance (highest current → growing)
export const sortMetricsPerformance = compose(
descending((m: Metric) => m.current),
descending((m: Metric) => m.changePercent)
)Event Calendar
interface CalendarEvent {
id: string
title: string
startDate: Date
endDate: Date
isAllDay: boolean
category: string
attendees: number
}
// Pattern 1: Chronological (start date → duration)
export const sortEventsChronological = compose(
byDate((e: CalendarEvent) => e.startDate),
ascending((e: CalendarEvent) => e.endDate.getTime() - e.startDate.getTime())
)
// Pattern 2: Upcoming (all-day last → start time)
export const sortEventsUpcoming = compose(
ascending((e: CalendarEvent) => e.isAllDay ? 1 : 0),
byDate((e: CalendarEvent) => e.startDate)
)
// Pattern 3: By category and time
export const sortEventsByCategory = compose(
ascending((e: CalendarEvent) => e.category),
byDate((e: CalendarEvent) => e.startDate)
)Leaderboard / Rankings
interface LeaderboardEntry {
rank: number
player: string
score: number
level: number
completionTime: number
achievements: number
}
// Pattern 1: By score (highest → fastest completion)
export const sortLeaderboardByScore = compose(
descending((e: LeaderboardEntry) => e.score),
ascending((e: LeaderboardEntry) => e.completionTime)
)
// Pattern 2: By level (highest → most achievements)
export const sortLeaderboardByLevel = compose(
descending((e: LeaderboardEntry) => e.level),
descending((e: LeaderboardEntry) => e.achievements),
ascending((e: LeaderboardEntry) => e.completionTime)
)
// Pattern 3: Speed run (fastest completion → highest score)
export const sortLeaderboardSpeedrun = compose(
ascending((e: LeaderboardEntry) => e.completionTime),
descending((e: LeaderboardEntry) => e.score)
)API Response Pagination
interface PaginatedRequest {
sortBy?: string
sortOrder?: 'asc' | 'desc'
page: number
pageSize: number
}
function buildAPIComparator<T extends Record<string, any>>(
request: PaginatedRequest
): Comparator<T> | null {
if (!request.sortBy) return null
const extractor = (item: T) => item[request.sortBy!]
const base = ascending(extractor)
return request.sortOrder === 'desc' ? reverse(base) : base
}
// Usage in API handler
async function handleListRequest(req: PaginatedRequest) {
let items = await fetchItems()
const comparator = buildAPIComparator(req)
if (comparator) {
items = items.sort(comparator)
}
return paginate(items, { page: req.page, pageSize: req.pageSize })
}Next Steps
- Migration Guide - Converting existing sort code
- API Reference - Complete function catalog