mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 19:24:03 +00:00
103 lines
2.3 KiB
TypeScript
103 lines
2.3 KiB
TypeScript
import type { Instant } from './time'
|
|
|
|
import { DOMAIN_ERROR_CODE, DomainError } from './errors'
|
|
|
|
const BILLING_PERIOD_PATTERN = /^(\d{4})-(\d{2})$/
|
|
|
|
function isIntegerInRange(value: number, min: number, max: number): boolean {
|
|
return Number.isInteger(value) && value >= min && value <= max
|
|
}
|
|
|
|
export class BillingPeriod {
|
|
readonly year: number
|
|
readonly month: number
|
|
|
|
private constructor(year: number, month: number) {
|
|
this.year = year
|
|
this.month = month
|
|
}
|
|
|
|
static from(year: number, month: number): BillingPeriod {
|
|
if (!isIntegerInRange(year, 1970, 9999)) {
|
|
throw new DomainError(
|
|
DOMAIN_ERROR_CODE.INVALID_BILLING_PERIOD,
|
|
`Invalid billing year: ${year}`
|
|
)
|
|
}
|
|
|
|
if (!isIntegerInRange(month, 1, 12)) {
|
|
throw new DomainError(
|
|
DOMAIN_ERROR_CODE.INVALID_BILLING_PERIOD,
|
|
`Invalid billing month: ${month}`
|
|
)
|
|
}
|
|
|
|
return new BillingPeriod(year, month)
|
|
}
|
|
|
|
static fromString(value: string): BillingPeriod {
|
|
const match = BILLING_PERIOD_PATTERN.exec(value)
|
|
|
|
if (!match) {
|
|
throw new DomainError(
|
|
DOMAIN_ERROR_CODE.INVALID_BILLING_PERIOD,
|
|
`Billing period must match YYYY-MM: ${value}`
|
|
)
|
|
}
|
|
|
|
const [, yearString, monthString] = match
|
|
|
|
return BillingPeriod.from(Number(yearString), Number(monthString))
|
|
}
|
|
|
|
static fromInstant(instant: Instant): BillingPeriod {
|
|
const zoned = instant.toZonedDateTimeISO('UTC')
|
|
|
|
return BillingPeriod.from(zoned.year, zoned.month)
|
|
}
|
|
|
|
next(): BillingPeriod {
|
|
if (this.month === 12) {
|
|
return BillingPeriod.from(this.year + 1, 1)
|
|
}
|
|
|
|
return BillingPeriod.from(this.year, this.month + 1)
|
|
}
|
|
|
|
previous(): BillingPeriod {
|
|
if (this.month === 1) {
|
|
return BillingPeriod.from(this.year - 1, 12)
|
|
}
|
|
|
|
return BillingPeriod.from(this.year, this.month - 1)
|
|
}
|
|
|
|
compare(other: BillingPeriod): -1 | 0 | 1 {
|
|
if (this.year < other.year) {
|
|
return -1
|
|
}
|
|
|
|
if (this.year > other.year) {
|
|
return 1
|
|
}
|
|
|
|
if (this.month < other.month) {
|
|
return -1
|
|
}
|
|
|
|
if (this.month > other.month) {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
equals(other: BillingPeriod): boolean {
|
|
return this.year === other.year && this.month === other.month
|
|
}
|
|
|
|
toString(): string {
|
|
return `${this.year.toString().padStart(4, '0')}-${this.month.toString().padStart(2, '0')}`
|
|
}
|
|
}
|