Skip to content

Commit 58d4a51

Browse files
authored
Merge pull request #8202 from woocommerce/issue/8149-delta-percentage-value
2 parents bc2ae95 + 1dc6560 commit 58d4a51

File tree

2 files changed

+131
-54
lines changed

2 files changed

+131
-54
lines changed

WooCommerce/Classes/ViewRelated/Dashboard/Factories/StatsDataTextFormatter.swift

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ struct StatsDataTextFormatter {
2626

2727
/// Creates the text to display for the total revenue delta.
2828
///
29-
static func createTotalRevenueDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> String {
30-
if let previousRevenue = totalRevenue(at: nil, orderStats: previousPeriod), let currentRevenue = totalRevenue(at: nil, orderStats: currentPeriod) {
31-
return createDeltaText(from: previousRevenue, to: currentRevenue)
32-
} else {
33-
return Constants.placeholderText
34-
}
29+
static func createTotalRevenueDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> DeltaPercentage {
30+
let previousRevenue = totalRevenue(at: nil, orderStats: previousPeriod)
31+
let currentRevenue = totalRevenue(at: nil, orderStats: currentPeriod)
32+
return createDeltaPercentage(from: previousRevenue, to: currentRevenue)
3533
}
3634

3735
// MARK: Orders Stats
@@ -48,12 +46,10 @@ struct StatsDataTextFormatter {
4846

4947
/// Creates the text to display for the order count delta.
5048
///
51-
static func createOrderCountDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> String {
52-
if let previousCount = orderCount(at: nil, orderStats: previousPeriod), let currentCount = orderCount(at: nil, orderStats: currentPeriod) {
53-
return createDeltaText(from: previousCount, to: currentCount)
54-
} else {
55-
return Constants.placeholderText
56-
}
49+
static func createOrderCountDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> DeltaPercentage {
50+
let previousCount = orderCount(at: nil, orderStats: previousPeriod)
51+
let currentCount = orderCount(at: nil, orderStats: currentPeriod)
52+
return createDeltaPercentage(from: previousCount, to: currentCount)
5753
}
5854

5955
/// Creates the text to display for the average order value.
@@ -70,12 +66,10 @@ struct StatsDataTextFormatter {
7066

7167
/// Creates the text to display for the average order value delta.
7268
///
73-
static func createAverageOrderValueDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> String {
74-
if let previousAverage = averageOrderValue(orderStats: previousPeriod), let currentAverage = averageOrderValue(orderStats: currentPeriod) {
75-
return createDeltaText(from: previousAverage, to: currentAverage)
76-
} else {
77-
return Constants.placeholderText
78-
}
69+
static func createAverageOrderValueDelta(from previousPeriod: OrderStatsV4?, to currentPeriod: OrderStatsV4?) -> DeltaPercentage {
70+
let previousAverage = averageOrderValue(orderStats: previousPeriod)
71+
let currentAverage = averageOrderValue(orderStats: currentPeriod)
72+
return createDeltaPercentage(from: previousAverage, to: currentAverage)
7973
}
8074

8175
// MARK: Views and Visitors Stats
@@ -92,12 +86,10 @@ struct StatsDataTextFormatter {
9286

9387
/// Creates the text to display for the visitor count delta.
9488
///
95-
static func createVisitorCountDelta(from previousPeriod: SiteVisitStats?, to currentPeriod: SiteVisitStats?) -> String {
96-
if let previousCount = visitorCount(at: nil, siteStats: previousPeriod), let currentCount = visitorCount(at: nil, siteStats: currentPeriod) {
97-
return createDeltaText(from: previousCount, to: currentCount)
98-
} else {
99-
return Constants.placeholderText
100-
}
89+
static func createVisitorCountDelta(from previousPeriod: SiteVisitStats?, to currentPeriod: SiteVisitStats?) -> DeltaPercentage {
90+
let previousCount = visitorCount(at: nil, siteStats: previousPeriod)
91+
let currentCount = visitorCount(at: nil, siteStats: currentPeriod)
92+
return createDeltaPercentage(from: previousCount, to: currentCount)
10193
}
10294

10395
// MARK: Conversion Stats
@@ -128,21 +120,59 @@ extension StatsDataTextFormatter {
128120

129121
// MARK: Delta Calculations
130122

131-
/// Creates the text showing the percent change from the previous `Decimal` value to the current `Decimal` value
123+
/// Creates the `DeltaPercentage` for the percent change from the previous `Decimal` value to the current `Decimal` value
132124
///
133-
static func createDeltaText(from previousValue: Decimal, to currentValue: Decimal) -> String {
134-
guard previousValue > 0 else {
135-
return deltaNumberFormatter.string(from: 1) ?? "+100%"
125+
static func createDeltaPercentage(from previousValue: Decimal?, to currentValue: Decimal?) -> DeltaPercentage {
126+
guard let previousValue, let currentValue, previousValue != currentValue else {
127+
return DeltaPercentage(value: 0) // Missing or equal values: 0% change
136128
}
137129

138-
let deltaValue = ((currentValue - previousValue) / previousValue)
139-
return deltaNumberFormatter.string(from: deltaValue as NSNumber) ?? Constants.placeholderText
130+
// If the previous value was 0, return a 100% or -100% change
131+
guard previousValue != 0 else {
132+
let deltaValue: Decimal = currentValue > 0 ? 1 : -1
133+
return DeltaPercentage(value: deltaValue)
134+
}
135+
136+
return DeltaPercentage(value: (currentValue - previousValue) / previousValue)
140137
}
141138

142-
/// Creates the text showing the percent change from the previous `Double` value to the current `Double` value
139+
/// Creates the `DeltaPercentage` for the percent change from the previous `Double` value to the current `Double` value
143140
///
144-
static func createDeltaText(from previousValue: Double, to currentValue: Double) -> String {
145-
createDeltaText(from: Decimal(previousValue), to: Decimal(currentValue))
141+
static func createDeltaPercentage(from previousValue: Double?, to currentValue: Double?) -> DeltaPercentage {
142+
guard let previousValue, let currentValue else {
143+
return DeltaPercentage(value: 0) // Missing data: 0% change
144+
}
145+
146+
return createDeltaPercentage(from: Decimal(previousValue), to: Decimal(currentValue))
147+
}
148+
149+
/// Represents a formatted delta percentage string and its direction of change
150+
struct DeltaPercentage {
151+
/// The delta percentage formatted as a localized string (e.g. `+100%`)
152+
let string: String
153+
154+
/// The direction of change
155+
let direction: Direction
156+
157+
init(value: Decimal) {
158+
self.string = deltaNumberFormatter.string(from: value as NSNumber) ?? Constants.placeholderText
159+
self.direction = {
160+
if value > 0 {
161+
return .positive
162+
} else if value < 0 {
163+
return .negative
164+
} else {
165+
return .zero
166+
}
167+
}()
168+
}
169+
170+
/// Represents the direction of change for a delta value
171+
enum Direction {
172+
case positive
173+
case negative
174+
case zero
175+
}
146176
}
147177

148178
// MARK: Stats Intervals

WooCommerce/WooCommerceTests/ViewRelated/Dashboard/StatsDataTextFormatterTests.swift

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ final class StatsDataTextFormatterTests: XCTestCase {
6262
XCTAssertEqual(totalRevenue, "$25")
6363
}
6464

65-
func test_createTotalRevenueDelta_returns_expected_delta_text() {
65+
func test_createTotalRevenueDelta_returns_expected_delta() {
6666
// Given
6767
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(grossRevenue: 10))
6868
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(grossRevenue: 15))
@@ -71,7 +71,8 @@ final class StatsDataTextFormatterTests: XCTestCase {
7171
let totalRevenueDelta = StatsDataTextFormatter.createTotalRevenueDelta(from: previousOrderStats, to: currentOrderStats)
7272

7373
// Then
74-
XCTAssertEqual(totalRevenueDelta, "+50%")
74+
XCTAssertEqual(totalRevenueDelta.string, "+50%")
75+
XCTAssertEqual(totalRevenueDelta.direction, .positive)
7576
}
7677

7778
// MARK: Orders Stats
@@ -106,7 +107,7 @@ final class StatsDataTextFormatterTests: XCTestCase {
106107
XCTAssertEqual(orderCount, "1")
107108
}
108109

109-
func test_createOrderCountDelta_returns_expected_delta_text() {
110+
func test_createOrderCountDelta_returns_expected_delta() {
110111
// Given
111112
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 10))
112113
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 15))
@@ -115,7 +116,8 @@ final class StatsDataTextFormatterTests: XCTestCase {
115116
let orderCountDelta = StatsDataTextFormatter.createOrderCountDelta(from: previousOrderStats, to: currentOrderStats)
116117

117118
// Then
118-
XCTAssertEqual(orderCountDelta, "+50%")
119+
XCTAssertEqual(orderCountDelta.string, "+50%")
120+
XCTAssertEqual(orderCountDelta.direction, .positive)
119121
}
120122

121123
func test_createAverageOrderValueText_does_not_return_decimal_points_for_integer_value() {
@@ -144,7 +146,7 @@ final class StatsDataTextFormatterTests: XCTestCase {
144146
XCTAssertEqual(averageOrderValue, "$62.86")
145147
}
146148

147-
func test_createAverageOrderValueDelta_returns_expected_delta_text() {
149+
func test_createAverageOrderValueDelta_returns_expected_delta() {
148150
// Given
149151
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(averageOrderValue: 10.00))
150152
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(averageOrderValue: 15.00))
@@ -153,7 +155,8 @@ final class StatsDataTextFormatterTests: XCTestCase {
153155
let averageOrderValueDelta = StatsDataTextFormatter.createAverageOrderValueDelta(from: previousOrderStats, to: currentOrderStats)
154156

155157
// Then
156-
XCTAssertEqual(averageOrderValueDelta, "+50%")
158+
XCTAssertEqual(averageOrderValueDelta.string, "+50%")
159+
XCTAssertEqual(averageOrderValueDelta.direction, .positive)
157160
}
158161

159162
// MARK: Views and Visitors Stats
@@ -186,7 +189,7 @@ final class StatsDataTextFormatterTests: XCTestCase {
186189
XCTAssertEqual(visitorCount, "17")
187190
}
188191

189-
func test_createVisitorCountDelta_returns_expected_delta_text() {
192+
func test_createVisitorCountDelta_returns_expected_delta() {
190193
// Given
191194
let previousSiteStats = SiteVisitStats.fake().copy(items: [.fake().copy(period: "0", visitors: 10)])
192195
let currentSiteStats = SiteVisitStats.fake().copy(items: [.fake().copy(period: "0", visitors: 15)])
@@ -195,7 +198,8 @@ final class StatsDataTextFormatterTests: XCTestCase {
195198
let visitorCountDelta = StatsDataTextFormatter.createVisitorCountDelta(from: previousSiteStats, to: currentSiteStats)
196199

197200
// Then
198-
XCTAssertEqual(visitorCountDelta, "+50%")
201+
XCTAssertEqual(visitorCountDelta.string, "+50%")
202+
XCTAssertEqual(visitorCountDelta.direction, .positive)
199203
}
200204

201205
// MARK: Conversion Stats
@@ -251,51 +255,94 @@ final class StatsDataTextFormatterTests: XCTestCase {
251255

252256
// MARK: Delta Calculations
253257

254-
func test_createDeltaText_returns_expected_positive_text() {
258+
func test_createDeltaPercentage_returns_expected_positive_delta() {
255259
// Given
256260
let previousValue: Double = 100
257261
let currentValue: Double = 150
258262

259263
// When
260-
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
264+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
261265

262266
// Then
263-
XCTAssertEqual(deltaText, "+50%")
267+
XCTAssertEqual(delta.string, "+50%")
268+
XCTAssertEqual(delta.direction, .positive)
264269
}
265270

266-
func test_createDeltaText_returns_expected_negative_text() {
271+
func test_createDeltaPercentage_returns_expected_negative_delta() {
267272
// Given
268-
let previousValue: Double = 150
273+
let previousValue: Double = 100
274+
let currentValue: Double = 50
275+
276+
// When
277+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
278+
279+
// Then
280+
XCTAssertEqual(delta.string, "-50%")
281+
XCTAssertEqual(delta.direction, .negative)
282+
}
283+
284+
func test_createDeltaPercentage_returns_expected_zero_delta() {
285+
// Given
286+
let previousValue: Double = 100
269287
let currentValue: Double = 100
270288

271289
// When
272-
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
290+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
291+
292+
// Then
293+
XCTAssertEqual(delta.string, "+0%")
294+
XCTAssertEqual(delta.direction, .zero)
295+
}
296+
297+
func test_createDeltaPercentage_returns_expected_zero_delta_for_zero_values() {
298+
// Given
299+
let previousValue: Double = 0
300+
let currentValue: Double = 0
301+
302+
// When
303+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
273304

274305
// Then
275-
XCTAssertEqual(deltaText, "-33%")
306+
XCTAssertEqual(delta.string, "+0%")
307+
XCTAssertEqual(delta.direction, .zero)
276308
}
277309

278-
func test_createDeltaText_returns_100_percent_change_when_previous_value_is_zero() {
310+
func test_createDeltaPercentage_returns_positive_100_percent_change_when_previous_value_is_zero() {
279311
// Given
280312
let previousValue: Double = 0
281313
let currentValue: Double = 10
282314

283315
// When
284-
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
316+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
317+
318+
// Then
319+
XCTAssertEqual(delta.string, "+100%")
320+
XCTAssertEqual(delta.direction, .positive)
321+
}
322+
323+
func test_createDeltaPercentage_returns_negative_100_percent_change_when_previous_value_is_zero() {
324+
// Given
325+
let previousValue: Double = 0
326+
let currentValue: Double = -10
327+
328+
// When
329+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
285330

286331
// Then
287-
XCTAssertEqual(deltaText, "+100%")
332+
XCTAssertEqual(delta.string, "-100%")
333+
XCTAssertEqual(delta.direction, .negative)
288334
}
289335

290-
func test_createDeltaText_returns_negative_100_percent_change_when_current_value_is_zero() {
336+
func test_createDeltaPercentage_returns_negative_100_percent_change_when_current_value_is_zero() {
291337
// Given
292338
let previousValue: Double = 10
293339
let currentValue: Double = 0
294340

295341
// When
296-
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
342+
let delta = StatsDataTextFormatter.createDeltaPercentage(from: previousValue, to: currentValue)
297343

298344
// Then
299-
XCTAssertEqual(deltaText, "-100%")
345+
XCTAssertEqual(delta.string, "-100%")
346+
XCTAssertEqual(delta.direction, .negative)
300347
}
301348
}

0 commit comments

Comments
 (0)