diff --git a/.changeset/cost-insights-wise-moons-crash.md b/.changeset/cost-insights-wise-moons-crash.md new file mode 100644 index 0000000000..1277911059 --- /dev/null +++ b/.changeset/cost-insights-wise-moons-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Fixed date calculations incorrectly converting to UTC in some cases. This should be a transparent change. diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 7cc03caa0b..9516dfe270 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -76,13 +76,13 @@ describe('getPreviousPeriodTotalCost', () => { change: changeOf(MockAggregatedDailyCosts), trendline: trendlineOf(MockAggregatedDailyCosts), }; - const exclusiveEndDate = '2020-09-30'; + const inclusiveEndDate = '2020-09-30'; expect( getPreviousPeriodTotalCost( mockGroupDailyCost.aggregation, Duration.P30D, - exclusiveEndDate, + inclusiveEndDate, ), - ).toEqual(100_000); + ).toEqual(96_600); }); }); diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index fc50439aad..92057a560e 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -22,7 +22,6 @@ import { GrowthType, MetricData, Duration, - DEFAULT_DATE_FORMAT, DateAggregation, } from '../types'; import dayjs, { OpUnitType } from 'dayjs'; @@ -73,10 +72,7 @@ export function getPreviousPeriodTotalCost( inclusiveEndDate: string, ): number { const dayjsDuration = dayjs.duration(duration); - const startDate = inclusiveStartDateOf( - duration, - dayjs(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT), - ); + const startDate = inclusiveStartDateOf(duration, inclusiveEndDate); // dayjs doesn't allow adding an ISO 8601 period to dates. const [amount, type]: [number, OpUnitType] = dayjsDuration.days() ? [dayjsDuration.days(), 'day'] diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 79ea150fc5..27447537cc 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -24,23 +24,21 @@ export const DEFAULT_DURATION = Duration.P30D; * Derive the start date of a given period, assuming two repeating intervals. * * @param duration see comment on Duration enum - * @param exclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate + 1 day + * @param inclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate */ export function inclusiveStartDateOf( duration: Duration, - exclusiveEndDate: string, + inclusiveEndDate: string, ): string { switch (duration) { case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(exclusiveEndDate) - .utc() + return moment(inclusiveEndDate) .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(exclusiveEndDate) - .utc() + return moment(inclusiveEndDate) .startOf('quarter') .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); @@ -57,13 +55,9 @@ export function exclusiveEndDateOf( case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(inclusiveEndDate) - .utc() - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); + return moment(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT); case Duration.P3M: return moment(quarterEndDate(inclusiveEndDate)) - .utc() .add(1, 'day') .format(DEFAULT_DATE_FORMAT); default: @@ -76,7 +70,6 @@ export function inclusiveEndDateOf( inclusiveEndDate: string, ): string { return moment(exclusiveEndDateOf(duration, inclusiveEndDate)) - .utc() .subtract(1, 'day') .format(DEFAULT_DATE_FORMAT); } @@ -94,7 +87,7 @@ export function intervalsOf( } export function quarterEndDate(inclusiveEndDate: string): string { - const endDate = moment(inclusiveEndDate).utc(); + const endDate = moment(inclusiveEndDate); const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); if (endOfQuarter === inclusiveEndDate) { return endDate.format(DEFAULT_DATE_FORMAT); diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 182c567644..8f2b94f997 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -16,7 +16,7 @@ import moment from 'moment'; import pluralize from 'pluralize'; -import { Duration, DEFAULT_DATE_FORMAT } from '../types'; +import { Duration } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; export type Period = { @@ -92,11 +92,8 @@ export function formatPercent(n: number): string { } export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) { - const exclusiveEndDate = moment(inclusiveEndDate) - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); const start = moment( - inclusiveStartDateOf(Duration.P3M, exclusiveEndDate), + inclusiveStartDateOf(Duration.P3M, inclusiveEndDate), ).format('[Q]Q YYYY'); const end = moment(inclusiveEndDateOf(Duration.P3M, inclusiveEndDate)).format( '[Q]Q YYYY', diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 14ff9aecd1..d4ac4cd2df 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -36,7 +36,7 @@ import { getDefaultState as getDefaultLoadingState, } from '../utils/loading'; import { findAlways } from '../utils/assert'; -import { inclusiveStartDateOf } from './duration'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; type mockAlertRenderer = (alert: T) => T; type mockEntityRenderer = (entity: T) => T; @@ -228,8 +228,9 @@ export function aggregationFor( baseline: number, ): DateAggregation[] { const { duration, endDate } = parseIntervals(intervals); + const inclusiveEndDate = inclusiveEndDateOf(duration, endDate); const days = dayjs(endDate).diff( - inclusiveStartDateOf(duration, endDate), + inclusiveStartDateOf(duration, inclusiveEndDate), 'day', ); @@ -244,7 +245,7 @@ export function aggregationFor( return [...Array(days).keys()].reduce( (values: DateAggregation[], i: number): DateAggregation[] => { const last = values.length ? values[values.length - 1].amount : baseline; - const date = dayjs(inclusiveStartDateOf(duration, endDate)) + const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate)) .add(i, 'day') .format(DEFAULT_DATE_FORMAT); const amount = Math.max(0, last + nextDelta());