Skip to content

Commit db394a5

Browse files
authored
Merge pull request #8181 from woocommerce/issue/8149-delta-percentage-helper
[Analytics Hub] Add helpers for creating the delta percentage text
2 parents 02384e2 + 09bf974 commit db394a5

File tree

2 files changed

+170
-0
lines changed

2 files changed

+170
-0
lines changed

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ struct StatsDataTextFormatter {
2424
}
2525
}
2626

27+
/// Creates the text to display for the total revenue delta.
28+
///
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+
}
35+
}
36+
2737
// MARK: Orders Stats
2838

2939
/// Creates the text to display for the order count.
@@ -36,6 +46,16 @@ struct StatsDataTextFormatter {
3646
}
3747
}
3848

49+
/// Creates the text to display for the order count delta.
50+
///
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+
}
57+
}
58+
3959
/// Creates the text to display for the average order value.
4060
///
4161
static func createAverageOrderValueText(orderStats: OrderStatsV4?, currencyFormatter: CurrencyFormatter, currencyCode: String) -> String {
@@ -48,6 +68,16 @@ struct StatsDataTextFormatter {
4868
}
4969
}
5070

71+
/// Creates the text to display for the average order value delta.
72+
///
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+
}
79+
}
80+
5181
// MARK: Views and Visitors Stats
5282

5383
/// Creates the text to display for the visitor count.
@@ -60,6 +90,16 @@ struct StatsDataTextFormatter {
6090
}
6191
}
6292

93+
/// Creates the text to display for the visitor count delta.
94+
///
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+
}
101+
}
102+
63103
// MARK: Conversion Stats
64104

65105
/// Creates the text to display for the conversion rate.
@@ -82,6 +122,28 @@ struct StatsDataTextFormatter {
82122
return Constants.placeholderText
83123
}
84124
}
125+
}
126+
127+
extension StatsDataTextFormatter {
128+
129+
// MARK: Delta Calculations
130+
131+
/// Creates the text showing the percent change from the previous `Decimal` value to the current `Decimal` value
132+
///
133+
static func createDeltaText(from previousValue: Decimal, to currentValue: Decimal) -> String {
134+
guard previousValue > 0 else {
135+
return deltaNumberFormatter.string(from: 1) ?? "+100%"
136+
}
137+
138+
let deltaValue = ((currentValue - previousValue) / previousValue)
139+
return deltaNumberFormatter.string(from: deltaValue as NSNumber) ?? Constants.placeholderText
140+
}
141+
142+
/// Creates the text showing the percent change from the previous `Double` value to the current `Double` value
143+
///
144+
static func createDeltaText(from previousValue: Double, to currentValue: Double) -> String {
145+
createDeltaText(from: Decimal(previousValue), to: Decimal(currentValue))
146+
}
85147

86148
// MARK: Stats Intervals
87149

@@ -98,6 +160,16 @@ struct StatsDataTextFormatter {
98160
// MARK: - Private helpers
99161

100162
private extension StatsDataTextFormatter {
163+
164+
/// Number formatter for delta percentages, e.g. `+36%` or `-16%`.
165+
///
166+
static let deltaNumberFormatter: NumberFormatter = {
167+
let numberFormatter = NumberFormatter()
168+
numberFormatter.numberStyle = .percent
169+
numberFormatter.positivePrefix = numberFormatter.plusSign
170+
return numberFormatter
171+
}()
172+
101173
/// Retrieves the visitor count for the provided order stats and, optionally, a specific interval.
102174
///
103175
static func visitorCount(at selectedIndex: Int?, siteStats: SiteVisitStats?) -> Double? {

WooCommerce/WooCommerceTests/ViewRelated/Dashboard/StatsDataTextFormatterTests.swift

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

65+
func test_createTotalRevenueDelta_returns_expected_delta_text() {
66+
// Given
67+
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(grossRevenue: 10))
68+
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(grossRevenue: 15))
69+
70+
// When
71+
let totalRevenueDelta = StatsDataTextFormatter.createTotalRevenueDelta(from: previousOrderStats, to: currentOrderStats)
72+
73+
// Then
74+
XCTAssertEqual(totalRevenueDelta, "+50%")
75+
}
76+
6577
// MARK: Orders Stats
6678

6779
func test_createOrderCountText_returns_expected_order_count() {
@@ -94,6 +106,18 @@ final class StatsDataTextFormatterTests: XCTestCase {
94106
XCTAssertEqual(orderCount, "1")
95107
}
96108

109+
func test_createOrderCountDelta_returns_expected_delta_text() {
110+
// Given
111+
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 10))
112+
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(totalOrders: 15))
113+
114+
// When
115+
let orderCountDelta = StatsDataTextFormatter.createOrderCountDelta(from: previousOrderStats, to: currentOrderStats)
116+
117+
// Then
118+
XCTAssertEqual(orderCountDelta, "+50%")
119+
}
120+
97121
func test_createAverageOrderValueText_does_not_return_decimal_points_for_integer_value() {
98122
// Given
99123
let orderStats = OrderStatsV4.fake().copy(totals: .fake().copy(averageOrderValue: 62))
@@ -120,6 +144,18 @@ final class StatsDataTextFormatterTests: XCTestCase {
120144
XCTAssertEqual(averageOrderValue, "$62.86")
121145
}
122146

147+
func test_createAverageOrderValueDelta_returns_expected_delta_text() {
148+
// Given
149+
let previousOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(averageOrderValue: 10.00))
150+
let currentOrderStats = OrderStatsV4.fake().copy(totals: .fake().copy(averageOrderValue: 15.00))
151+
152+
// When
153+
let averageOrderValueDelta = StatsDataTextFormatter.createAverageOrderValueDelta(from: previousOrderStats, to: currentOrderStats)
154+
155+
// Then
156+
XCTAssertEqual(averageOrderValueDelta, "+50%")
157+
}
158+
123159
// MARK: Views and Visitors Stats
124160

125161
// This test reflects the current method for computing total visitor count.
@@ -150,6 +186,18 @@ final class StatsDataTextFormatterTests: XCTestCase {
150186
XCTAssertEqual(visitorCount, "17")
151187
}
152188

189+
func test_createVisitorCountDelta_returns_expected_delta_text() {
190+
// Given
191+
let previousSiteStats = SiteVisitStats.fake().copy(items: [.fake().copy(period: "0", visitors: 10)])
192+
let currentSiteStats = SiteVisitStats.fake().copy(items: [.fake().copy(period: "0", visitors: 15)])
193+
194+
// When
195+
let visitorCountDelta = StatsDataTextFormatter.createVisitorCountDelta(from: previousSiteStats, to: currentSiteStats)
196+
197+
// Then
198+
XCTAssertEqual(visitorCountDelta, "+50%")
199+
}
200+
153201
// MARK: Conversion Stats
154202

155203
func test_createConversionRateText_returns_placeholder_when_visitor_count_is_zero() {
@@ -200,4 +248,54 @@ final class StatsDataTextFormatterTests: XCTestCase {
200248
// Then
201249
XCTAssertEqual(conversionRate, "10%")
202250
}
251+
252+
// MARK: Delta Calculations
253+
254+
func test_createDeltaText_returns_expected_positive_text() {
255+
// Given
256+
let previousValue: Double = 100
257+
let currentValue: Double = 150
258+
259+
// When
260+
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
261+
262+
// Then
263+
XCTAssertEqual(deltaText, "+50%")
264+
}
265+
266+
func test_createDeltaText_returns_expected_negative_text() {
267+
// Given
268+
let previousValue: Double = 150
269+
let currentValue: Double = 100
270+
271+
// When
272+
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
273+
274+
// Then
275+
XCTAssertEqual(deltaText, "-33%")
276+
}
277+
278+
func test_createDeltaText_returns_100_percent_change_when_previous_value_is_zero() {
279+
// Given
280+
let previousValue: Double = 0
281+
let currentValue: Double = 10
282+
283+
// When
284+
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
285+
286+
// Then
287+
XCTAssertEqual(deltaText, "+100%")
288+
}
289+
290+
func test_createDeltaText_returns_negative_100_percent_change_when_current_value_is_zero() {
291+
// Given
292+
let previousValue: Double = 10
293+
let currentValue: Double = 0
294+
295+
// When
296+
let deltaText = StatsDataTextFormatter.createDeltaText(from: previousValue, to: currentValue)
297+
298+
// Then
299+
XCTAssertEqual(deltaText, "-100%")
300+
}
203301
}

0 commit comments

Comments
 (0)