From 053fc56745b5cc4e6209222032934729b06da047 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 10:15:32 -0500 Subject: [PATCH 1/9] Move out utility functions for mock data to utils --- plugins/cost-insights/src/client.ts | 39 +++------------------ plugins/cost-insights/src/utils/mockData.ts | 30 ++++++++++++++++ 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 740a44fcbd..74d4954ea4 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -16,11 +16,9 @@ /* eslint-disable no-restricted-imports */ import dayjs from 'dayjs'; -import regression, { DataPoint } from 'regression'; import { CostInsightsApi, ProductInsightsOptions } from '../src/api'; import { Alert, - ChangeStatistic, Cost, DateAggregation, DEFAULT_DATE_FORMAT, @@ -30,24 +28,21 @@ import { MetricData, Project, ProjectGrowthData, - Trendline, UnlabeledDataflowData, } from '../src/types'; import { ProjectGrowthAlert, UnlabeledDataflowAlert, } from '../src/utils/alerts'; -import { - exclusiveEndDateOf, - inclusiveStartDateOf, -} from '../src/utils/duration'; +import { inclusiveStartDateOf } from '../src/utils/duration'; +import { trendlineOf, changeOf } from './utils/mockData'; type IntervalFields = { duration: Duration; endDate: string; }; -function parseIntervals(intervals: string): IntervalFields { +export function parseIntervals(intervals: string): IntervalFields { const match = intervals.match( /\/(?P\d+[DM])\/(?\d{4}-\d{2}-\d{2})/, ); @@ -66,7 +61,7 @@ function aggregationFor( baseline: number, ): DateAggregation[] { const { duration, endDate } = parseIntervals(intervals); - const days = dayjs(exclusiveEndDateOf(duration, endDate)).diff( + const days = dayjs(endDate).diff( inclusiveStartDateOf(duration, endDate), 'day', ); @@ -86,32 +81,6 @@ function aggregationFor( ); } -function trendlineOf(aggregation: DateAggregation[]): Trendline { - const data: ReadonlyArray = aggregation.map(a => [ - Date.parse(a.date) / 1000, - a.amount, - ]); - const result = regression.linear(data, { precision: 5 }); - return { - slope: result.equation[0], - intercept: result.equation[1], - }; -} - -function changeOf(aggregation: DateAggregation[]): ChangeStatistic { - const half = Math.ceil(aggregation.length / 2); - const before = aggregation - .slice(0, half) - .reduce((sum, a) => sum + a.amount, 0); - const after = aggregation - .slice(half, aggregation.length) - .reduce((sum, a) => sum + a.amount, 0); - return { - ratio: (after - before) / before, - amount: after - before, - }; -} - export class ExampleCostInsightsClient implements CostInsightsApi { private request(_: any, res: any): Promise { return new Promise(resolve => setTimeout(resolve, 0, res)); diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index ddeb4636b9..f5ad21854a 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -14,16 +14,20 @@ * limitations under the License. */ +import regression, { DataPoint } from 'regression'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/core'; import { + ChangeStatistic, Duration, Entity, Product, ProductFilters, ProjectGrowthData, + Trendline, UnlabeledDataflowAlertProject, UnlabeledDataflowData, + DateAggregation, } from '../types'; import { DefaultLoadingAction, @@ -185,3 +189,29 @@ export const MockCostInsightsConfig: Partial = { getConfig: () => MockProductsConfig as Config, getOptionalConfig: () => MockMetricsConfig as Config, }; + +export function trendlineOf(aggregation: DateAggregation[]): Trendline { + const data: ReadonlyArray = aggregation.map(a => [ + Date.parse(a.date) / 1000, + a.amount, + ]); + const result = regression.linear(data, { precision: 5 }); + return { + slope: result.equation[0], + intercept: result.equation[1], + }; +} + +export function changeOf(aggregation: DateAggregation[]): ChangeStatistic { + const half = Math.ceil(aggregation.length / 2); + const before = aggregation + .slice(0, half) + .reduce((sum, a) => sum + a.amount, 0); + const after = aggregation + .slice(half, aggregation.length) + .reduce((sum, a) => sum + a.amount, 0); + return { + ratio: (after - before) / before, + amount: after - before, + }; +} From 3ec3fc48a7a7065e454309109a9f50b94c0ca4e2 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 10:15:50 -0500 Subject: [PATCH 2/9] Fix compared change calculation --- plugins/cost-insights/src/utils/change.ts | 24 +++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index cc797bb3b0..79f653158a 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -21,8 +21,10 @@ import { EngineerThreshold, GrowthType, MetricData, + Duration, } from '../types'; import { aggregationSort } from '../utils/sort'; +import moment from 'moment'; // Used for displaying status colors export function growthOf(ratio: number, amount?: number) { @@ -45,11 +47,29 @@ export function growthOf(ratio: number, amount?: number) { export function getComparedChange( dailyCost: Cost, metricData: MetricData, + duration: Duration, ): ChangeStatistic { const ratio = dailyCost.change.ratio - metricData.change.ratio; - const amount = dailyCost.aggregation.slice().sort(aggregationSort)[0].amount; + const previousPeriodTotal = getPreviousPeriodTotalCost(dailyCost, duration); return { ratio: ratio, - amount: amount * ratio, + amount: previousPeriodTotal * ratio, }; } + +export function getPreviousPeriodTotalCost( + dailyCost: Cost, + duration: Duration, +): number { + const costsByDate = dailyCost.aggregation.slice().sort(aggregationSort); + const nextPeriodStart = moment(costsByDate[0].date).add( + moment.duration(duration), + ); + + // Add up costs that incurred before the start of the next period. + return costsByDate.reduce((acc, costByDate) => { + return moment(costByDate.date).isBefore(nextPeriodStart) + ? acc + costByDate.amount + : acc; + }, 0); +} From 83e3df8a579020d8cfe17e4cac6453f557daa2b7 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 10:16:38 -0500 Subject: [PATCH 3/9] Add mock daily cost data for test purposes --- .../CostOverviewCard/CostOverviewCard.tsx | 2 +- .../cost-insights/src/utils/change.test.ts | 25 +- plugins/cost-insights/src/utils/mockData.ts | 247 ++++++++++++++++++ 3 files changed, 271 insertions(+), 3 deletions(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 8f52a0070b..358c7a98aa 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -50,7 +50,7 @@ export const CostOverviewCard = ({ ? findAlways(config.metrics, m => m.kind === filters.metric) : null; const comparedChange = metricData - ? getComparedChange(dailyCostData, metricData) + ? getComparedChange(dailyCostData, metricData, filters.duration) : null; return ( diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index daf4538f2c..709f2dd882 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -14,8 +14,15 @@ * limitations under the License. */ -import { growthOf } from './change'; -import { GrowthType, ChangeThreshold, EngineerThreshold } from '../types'; +import { growthOf, getPreviousPeriodTotalCost } from './change'; +import { + GrowthType, + ChangeThreshold, + EngineerThreshold, + Duration, + Cost, +} from '../types'; +import { MockAggregatedDailyCosts, trendlineOf, changeOf } from './mockData'; const GrowthMap = { [GrowthType.Negligible]: 'negligible growth', @@ -60,3 +67,17 @@ describe.each` }); }, ); + +describe('getPreviousPeriodTotalCost', () => { + it('Correctly returns the total cost for the previous period given daily costs', () => { + const mockGroupDailyCost: Cost = { + id: 'test-group', + aggregation: MockAggregatedDailyCosts, + change: changeOf(MockAggregatedDailyCosts), + trendline: trendlineOf(MockAggregatedDailyCosts), + }; + expect( + getPreviousPeriodTotalCost(mockGroupDailyCost, Duration.P1M), + ).toEqual(100_000); + }); +}); diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index f5ad21854a..708ce733dd 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -215,3 +215,250 @@ export function changeOf(aggregation: DateAggregation[]): ChangeStatistic { amount: after - before, }; } + +export const MockAggregatedDailyCosts: DateAggregation[] = [ + { + date: '2020-08-07', + amount: 3500, + }, + { + date: '2020-08-06', + amount: 2500, + }, + { + date: '2020-08-05', + amount: 1400, + }, + { + date: '2020-08-04', + amount: 3800, + }, + { + date: '2020-08-09', + amount: 1900, + }, + { + date: '2020-08-08', + amount: 2400, + }, + { + date: '2020-08-03', + amount: 4000, + }, + { + date: '2020-08-02', + amount: 3700, + }, + { + date: '2020-08-01', + amount: 2500, + }, + { + date: '2020-08-18', + amount: 4300, + }, + { + date: '2020-08-17', + amount: 1500, + }, + { + date: '2020-08-16', + amount: 3600, + }, + { + date: '2020-08-15', + amount: 2200, + }, + { + date: '2020-08-19', + amount: 3900, + }, + { + date: '2020-08-10', + amount: 4100, + }, + { + date: '2020-08-14', + amount: 3600, + }, + { + date: '2020-08-13', + amount: 2900, + }, + { + date: '2020-08-12', + amount: 2700, + }, + { + date: '2020-08-11', + amount: 5100, + }, + { + date: '2020-09-19', + amount: 1200, + }, + { + date: '2020-09-18', + amount: 6500, + }, + { + date: '2020-09-17', + amount: 2500, + }, + { + date: '2020-09-16', + amount: 1400, + }, + { + date: '2020-09-11', + amount: 2300, + }, + { + date: '2020-09-10', + amount: 1900, + }, + { + date: '2020-09-15', + amount: 3100, + }, + { + date: '2020-09-14', + amount: 4500, + }, + { + date: '2020-09-13', + amount: 3300, + }, + { + date: '2020-09-12', + amount: 2800, + }, + { + date: '2020-09-29', + amount: 2600, + }, + { + date: '2020-09-28', + amount: 4100, + }, + { + date: '2020-09-27', + amount: 3800, + }, + { + date: '2020-09-22', + amount: 3700, + }, + { + date: '2020-09-21', + amount: 2700, + }, + { + date: '2020-09-20', + amount: 2200, + }, + { + date: '2020-09-26', + amount: 3300, + }, + { + date: '2020-09-25', + amount: 4000, + }, + { + date: '2020-09-24', + amount: 3800, + }, + { + date: '2020-09-23', + amount: 4100, + }, + { + date: '2020-08-29', + amount: 4400, + }, + { + date: '2020-08-28', + amount: 5000, + }, + { + date: '2020-08-27', + amount: 4900, + }, + { + date: '2020-08-26', + amount: 4100, + }, + { + date: '2020-08-21', + amount: 3700, + }, + { + date: '2020-08-20', + amount: 2200, + }, + { + date: '2020-08-25', + amount: 1700, + }, + { + date: '2020-08-24', + amount: 2100, + }, + { + date: '2020-08-23', + amount: 3100, + }, + { + date: '2020-08-22', + amount: 1500, + }, + { + date: '2020-09-08', + amount: 2900, + }, + { + date: '2020-09-07', + amount: 4100, + }, + { + date: '2020-09-06', + amount: 3600, + }, + { + date: '2020-09-05', + amount: 3300, + }, + { + date: '2020-09-09', + amount: 2800, + }, + { + date: '2020-08-31', + amount: 3400, + }, + { + date: '2020-08-30', + amount: 4300, + }, + { + date: '2020-09-04', + amount: 6100, + }, + { + date: '2020-09-03', + amount: 2500, + }, + { + date: '2020-09-02', + amount: 4900, + }, + { + date: '2020-09-01', + amount: 6100, + }, + { + date: '2020-09-30', + amount: 5500, + }, +]; From 43396127893cd1d6162b7090f6b27bec18bddc3c Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:15:58 -0500 Subject: [PATCH 4/9] Update compared change calculation to use last billing date --- plugins/cost-insights/src/utils/change.ts | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index 79f653158a..a5a0a86f0a 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -25,6 +25,11 @@ import { } from '../types'; import { aggregationSort } from '../utils/sort'; import moment from 'moment'; +import dayjs, { OpUnitType } from 'dayjs'; +import duration, { DurationInputType } from 'dayjs/plugin/duration'; +import { inclusiveStartDateOf } from './duration'; + +dayjs.extend(duration); // Used for displaying status colors export function growthOf(ratio: number, amount?: number) { @@ -48,9 +53,14 @@ export function getComparedChange( dailyCost: Cost, metricData: MetricData, duration: Duration, + lastCompleteBillingDate: string, // YYYY-MM-DD, ): ChangeStatistic { const ratio = dailyCost.change.ratio - metricData.change.ratio; - const previousPeriodTotal = getPreviousPeriodTotalCost(dailyCost, duration); + const previousPeriodTotal = getPreviousPeriodTotalCost( + dailyCost, + duration, + lastCompleteBillingDate, + ); return { ratio: ratio, amount: previousPeriodTotal * ratio, @@ -60,15 +70,18 @@ export function getComparedChange( export function getPreviousPeriodTotalCost( dailyCost: Cost, duration: Duration, + endDate: string, ): number { - const costsByDate = dailyCost.aggregation.slice().sort(aggregationSort); - const nextPeriodStart = moment(costsByDate[0].date).add( - moment.duration(duration), - ); + const dayjsDuration = dayjs.duration(duration); + const startDate = inclusiveStartDateOf(duration, endDate); + const [amount, type]: [number, OpUnitType] = dayjsDuration.days() + ? [dayjsDuration.days(), 'days' as OpUnitType] + : [dayjsDuration.months(), 'months' as OpUnitType]; + const nextPeriodStart = dayjs(startDate).add(amount, type); // Add up costs that incurred before the start of the next period. - return costsByDate.reduce((acc, costByDate) => { - return moment(costByDate.date).isBefore(nextPeriodStart) + return dailyCost.aggregation.reduce((acc, costByDate) => { + return dayjs(costByDate.date).isBefore(nextPeriodStart) ? acc + costByDate.amount : acc; }, 0); From 3a86e69f8cf773a1306828e9ab41150105a8336d Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:16:15 -0500 Subject: [PATCH 5/9] Fix cost chart tooltip date label --- .../src/components/CostOverviewCard/CostOverviewChart.tsx | 7 +++++-- plugins/cost-insights/src/utils/change.test.ts | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 6ed72923ff..1910013581 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ import React from 'react'; -import moment from 'moment'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; import { useTheme } from '@material-ui/core'; import { ComposedChart, @@ -50,6 +51,8 @@ import { useCostOverviewStyles as useStyles } from '../../utils/styles'; import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; +dayjs.extend(utc); + type CostOverviewChartProps = { metric: Maybe; metricData: Maybe; @@ -104,7 +107,7 @@ export const CostOverviewChart = ({ if (isInvalid({ label, payload })) return null; const dataKeys = [data.dailyCost.dataKey, data.metric.dataKey]; - const title = moment(label).format(DEFAULT_DATE_FORMAT); + const title = dayjs(label).utc().format(DEFAULT_DATE_FORMAT); const items = payload .filter(p => dataKeys.includes(p.dataKey as string)) .map(p => ({ diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 709f2dd882..94adc25f64 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -76,8 +76,13 @@ describe('getPreviousPeriodTotalCost', () => { change: changeOf(MockAggregatedDailyCosts), trendline: trendlineOf(MockAggregatedDailyCosts), }; + const exclusiveEndDate = '2020-10-01'; expect( - getPreviousPeriodTotalCost(mockGroupDailyCost, Duration.P1M), + getPreviousPeriodTotalCost( + mockGroupDailyCost, + Duration.P1M, + exclusiveEndDate, + ), ).toEqual(100_000); }); }); From da883b1fb11b57ea883985f7452cc507c2202dc1 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:16:32 -0500 Subject: [PATCH 6/9] Pass in last complete billing date to compared change calculation --- .../CostOverviewCard/CostOverviewCard.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 358c7a98aa..81dc118ea9 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -22,7 +22,12 @@ import { CostOverviewHeader } from './CostOverviewHeader'; import { LegendItem } from '../LegendItem'; import { MetricSelect } from '../MetricSelect'; import { PeriodSelect } from '../PeriodSelect'; -import { useScroll, useFilters, useConfig } from '../../hooks'; +import { + useScroll, + useFilters, + useConfig, + useLastCompleteBillingDate, +} from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; import { formatPercent } from '../../utils/formatters'; @@ -41,6 +46,7 @@ export const CostOverviewCard = ({ }: CostOverviewCardProps) => { const theme = useTheme(); const config = useConfig(); + const lastCompleteBillingDate = useLastCompleteBillingDate(); const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard); const { setDuration, setProject, setMetric, ...filters } = useFilters( mapFiltersToProps, @@ -50,7 +56,12 @@ export const CostOverviewCard = ({ ? findAlways(config.metrics, m => m.kind === filters.metric) : null; const comparedChange = metricData - ? getComparedChange(dailyCostData, metricData, filters.duration) + ? getComparedChange( + dailyCostData, + metricData, + filters.duration, + lastCompleteBillingDate, + ) : null; return ( From b13324a4de6910dc202e1ae17a55350cfbb8ad27 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:18:50 -0500 Subject: [PATCH 7/9] Fix ts errors --- plugins/cost-insights/src/utils/change.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index a5a0a86f0a..0fca672425 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -23,10 +23,8 @@ import { MetricData, Duration, } from '../types'; -import { aggregationSort } from '../utils/sort'; -import moment from 'moment'; import dayjs, { OpUnitType } from 'dayjs'; -import duration, { DurationInputType } from 'dayjs/plugin/duration'; +import duration from 'dayjs/plugin/duration'; import { inclusiveStartDateOf } from './duration'; dayjs.extend(duration); From 259d848eee9caf998ac102801f7189854d2f4c8d Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:21:24 -0500 Subject: [PATCH 8/9] Add changeset --- .changeset/cost-insights-wild-cars-wait.md | 5 +++++ plugins/cost-insights/src/client.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/cost-insights-wild-cars-wait.md diff --git a/.changeset/cost-insights-wild-cars-wait.md b/.changeset/cost-insights-wild-cars-wait.md new file mode 100644 index 0000000000..24f84dc56b --- /dev/null +++ b/.changeset/cost-insights-wild-cars-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Fix savings/excess display calculation diff --git a/plugins/cost-insights/src/client.ts b/plugins/cost-insights/src/client.ts index 74d4954ea4..2180385424 100644 --- a/plugins/cost-insights/src/client.ts +++ b/plugins/cost-insights/src/client.ts @@ -42,7 +42,7 @@ type IntervalFields = { endDate: string; }; -export function parseIntervals(intervals: string): IntervalFields { +function parseIntervals(intervals: string): IntervalFields { const match = intervals.match( /\/(?P\d+[DM])\/(?\d{4}-\d{2}-\d{2})/, ); From 319b6b17def15a7b7ad8fd03cc9a36a6e8a3cae6 Mon Sep 17 00:00:00 2001 From: Brenda Sukh Date: Fri, 13 Nov 2020 15:51:25 -0500 Subject: [PATCH 9/9] Make exclusive end dates explicit --- .../cost-insights/src/utils/change.test.ts | 2 +- plugins/cost-insights/src/utils/change.ts | 13 +++++--- plugins/cost-insights/src/utils/duration.ts | 32 +++++++++++-------- plugins/cost-insights/src/utils/formatters.ts | 24 ++++++++------ 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 94adc25f64..6a0a8e793b 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -76,7 +76,7 @@ describe('getPreviousPeriodTotalCost', () => { change: changeOf(MockAggregatedDailyCosts), trendline: trendlineOf(MockAggregatedDailyCosts), }; - const exclusiveEndDate = '2020-10-01'; + const exclusiveEndDate = '2020-09-30'; expect( getPreviousPeriodTotalCost( mockGroupDailyCost, diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index 0fca672425..1718462948 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -22,6 +22,7 @@ import { GrowthType, MetricData, Duration, + DEFAULT_DATE_FORMAT, } from '../types'; import dayjs, { OpUnitType } from 'dayjs'; import duration from 'dayjs/plugin/duration'; @@ -68,13 +69,17 @@ export function getComparedChange( export function getPreviousPeriodTotalCost( dailyCost: Cost, duration: Duration, - endDate: string, + inclusiveEndDate: string, ): number { const dayjsDuration = dayjs.duration(duration); - const startDate = inclusiveStartDateOf(duration, endDate); + const startDate = inclusiveStartDateOf( + duration, + dayjs(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT), + ); + // dayjs doesn't allow adding an ISO 8601 period to dates. const [amount, type]: [number, OpUnitType] = dayjsDuration.days() - ? [dayjsDuration.days(), 'days' as OpUnitType] - : [dayjsDuration.months(), 'months' as OpUnitType]; + ? [dayjsDuration.days(), 'day'] + : [dayjsDuration.months(), 'month']; const nextPeriodStart = dayjs(startDate).add(amount, type); // Add up costs that incurred before the start of the next period. diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 6a880b382c..ae8c255b91 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -22,27 +22,27 @@ import { assertNever } from './assert'; * Derive the start date of a given period, assuming two repeating intervals. * * @param duration see comment on Duration enum - * @param endDate from CostInsightsApi.getLastCompleteBillingDate + * @param endDate from CostInsightsApi.getLastCompleteBillingDate + 1 day */ export function inclusiveStartDateOf( duration: Duration, - endDate: string, + exclusiveEndDate: string, ): string { switch (duration) { case Duration.P30D: case Duration.P90D: - return moment(endDate) + return moment(exclusiveEndDate) .utc() .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); case Duration.P1M: - return moment(endDate) + return moment(exclusiveEndDate) .utc() .startOf('month') .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(endDate) + return moment(exclusiveEndDate) .utc() .startOf('quarter') .subtract(moment.duration(duration).add(moment.duration(duration))) @@ -54,16 +54,22 @@ export function inclusiveStartDateOf( export function exclusiveEndDateOf( duration: Duration, - endDate: string, + inclusiveEndDate: string, ): string { switch (duration) { case Duration.P30D: case Duration.P90D: - return moment(endDate).utc().add(1, 'day').format(DEFAULT_DATE_FORMAT); + return moment(inclusiveEndDate) + .utc() + .add(1, 'day') + .format(DEFAULT_DATE_FORMAT); case Duration.P1M: - return moment(endDate).utc().startOf('month').format(DEFAULT_DATE_FORMAT); + return moment(inclusiveEndDate) + .utc() + .startOf('month') + .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(endDate) + return moment(inclusiveEndDate) .utc() .startOf('quarter') .format(DEFAULT_DATE_FORMAT); @@ -74,15 +80,15 @@ export function exclusiveEndDateOf( export function inclusiveEndDateOf( duration: Duration, - endDate: string, + inclusiveEndDate: string, ): string { - return moment(exclusiveEndDateOf(duration, endDate)) + return moment(exclusiveEndDateOf(duration, inclusiveEndDate)) .utc() .subtract(1, 'day') .format(DEFAULT_DATE_FORMAT); } // https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals -export function intervalsOf(duration: Duration, endDate: string) { - return `R2/${duration}/${exclusiveEndDateOf(duration, endDate)}`; +export function intervalsOf(duration: Duration, inclusiveEndDate: string) { + return `R2/${duration}/${exclusiveEndDateOf(duration, inclusiveEndDate)}`; } diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index bf8e26ef5f..e81fc1f7c9 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -15,7 +15,7 @@ */ import moment from 'moment'; -import { Duration } from '../types'; +import { Duration, DEFAULT_DATE_FORMAT } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; import { pluralOf } from '../utils/grammar'; @@ -84,21 +84,27 @@ export function formatPercent(n: number): string { return `${(n * 100).toFixed(0)}%`; } -export function formatLastTwoLookaheadQuarters(endDate: string) { - const start = moment(inclusiveStartDateOf(Duration.P3M, endDate)).format( - '[Q]Q YYYY', - ); - const end = moment(inclusiveEndDateOf(Duration.P3M, endDate)).format( +export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) { + const exclusiveEndDate = moment(inclusiveEndDate) + .add(1, 'day') + .format(DEFAULT_DATE_FORMAT); + const start = moment( + inclusiveStartDateOf(Duration.P3M, exclusiveEndDate), + ).format('[Q]Q YYYY'); + const end = moment(inclusiveEndDateOf(Duration.P3M, inclusiveEndDate)).format( '[Q]Q YYYY', ); return `${start} vs ${end}`; } -export function formatLastTwoMonths(endDate: string) { - const start = moment(inclusiveStartDateOf(Duration.P1M, endDate)) +export function formatLastTwoMonths(inclusiveEndDate: string) { + const exclusiveEndDate = moment(inclusiveEndDate) + .add(1, 'day') + .format(DEFAULT_DATE_FORMAT); + const start = moment(inclusiveStartDateOf(Duration.P1M, exclusiveEndDate)) .utc() .format('MMMM'); - const end = moment(inclusiveEndDateOf(Duration.P1M, endDate)) + const end = moment(inclusiveEndDateOf(Duration.P1M, inclusiveEndDate)) .utc() .format('MMMM'); return `${start} vs ${end}`;