(
components: { key: K; dollars: number }[]
)
| 68 | * Each component's `dollars` is expected to already include any cost multiplier. |
| 69 | */ |
| 70 | export function apportionCredits<K extends string>( |
| 71 | components: { key: K; dollars: number }[] |
| 72 | ): Record<K, number> { |
| 73 | const result = {} as Record<K, number> |
| 74 | |
| 75 | const sanitized = components.map((c) => ({ |
| 76 | key: c.key, |
| 77 | dollars: Number.isFinite(c.dollars) && c.dollars > 0 ? c.dollars : 0, |
| 78 | })) |
| 79 | |
| 80 | const totalDollars = sanitized.reduce((sum, c) => sum + c.dollars, 0) |
| 81 | const targetCredits = dollarsToCredits(totalDollars) |
| 82 | |
| 83 | const exact = sanitized.map((c) => ({ |
| 84 | key: c.key, |
| 85 | floor: Math.floor(c.dollars * CREDIT_MULTIPLIER), |
| 86 | frac: c.dollars * CREDIT_MULTIPLIER - Math.floor(c.dollars * CREDIT_MULTIPLIER), |
| 87 | })) |
| 88 | |
| 89 | for (const c of exact) result[c.key] = c.floor |
| 90 | |
| 91 | let remainder = targetCredits - exact.reduce((sum, c) => sum + c.floor, 0) |
| 92 | const byFraction = [...exact].sort((a, b) => b.frac - a.frac) |
| 93 | for (let i = 0; i < byFraction.length && remainder > 0; i++) { |
| 94 | result[byFraction[i].key] += 1 |
| 95 | remainder-- |
| 96 | } |
| 97 | |
| 98 | return result |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * Format a dollar amount as a comma-separated credit string. |
no test coverage detected