Skip to content

Commit cd21a77

Browse files
igorlukaninmarianore-muttdata
authored andcommitted
docs: Add a recipe for 4-5-4 fiscal calendar
1 parent 6ec6bec commit cd21a77

File tree

4 files changed

+224
-1
lines changed

4 files changed

+224
-1
lines changed

docs/pages/guides/recipes.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ These recipes will show you the best practices of using Cube.
3434
- [Calculating filtered aggregates](/guides/recipes/data-modeling/filtered-aggregates)
3535
- [Calculating period-over-period changes](/guides/recipes/data-modeling/period-over-period)
3636
- [Implementing custom time dimension granularities](/guides/recipes/data-modeling/custom-granularity)
37+
- [Implementing custom calendars](/guides/recipes/data-modeling/custom-calendar)
3738
- [Implementing Entity-Attribute-Value model](/guides/recipes/data-modeling/entity-attribute-value)
3839
- [Implementing data snapshots](/guides/recipes/data-modeling/snapshots)
3940
- [Using different data models for tenants](/guides/recipes/access-control/using-different-schemas-for-tenants)

docs/pages/guides/recipes/data-modeling/_meta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
"filtered-aggregates": "Calculating filtered aggregates",
55
"period-over-period": "Calculating period-over-period changes",
66
"custom-granularity": "Implementing custom time dimension granularities",
7+
"custom-calendar": "Implementing custom calendars",
78
"snapshots": "Implementing data snapshots",
89
"entity-attribute-value": "Implementing Entity-Attribute-Value Model (EAV)",
910
"passing-dynamic-parameters-in-a-query": "Passing dynamic parameters in a query",
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Implementing custom calendars
2+
3+
This recipe explains the implementation of the [4-5-4 calendar][link-454], a common
4+
retail calendar used in the US and Canada. However, the same approach can be used
5+
to implement other custom calendars.
6+
7+
Unlike [custom time dimension granularities][ref-custom-granularities], custom
8+
calendars provide more flexibility and can be used when time units have variable
9+
lengths, such as the months and quarters in the 4-5-4 calendar. See the [custom
10+
granularities recipe][ref-custom-granularities-recipe] for more information.
11+
12+
## Use case
13+
14+
The 4-5-4 calendar ensures sales comparability between years by dividing the year
15+
into months based on a 4 weeks – 5 weeks – 4 weeks format. The layout of the calendar
16+
lines up holidays and ensures the same number of Saturdays and Sundays in comparable
17+
months. Hence, like days are compared to like days for sales reporting purposes.
18+
19+
## Data modeling
20+
21+
The data modeling includes the following steps:
22+
23+
* Create a calendar cube, e.g., `calendar_454`.
24+
* Extend it a number of times, so there's one calendar cube for every time dimension
25+
in cubes with facts that needs translation to a custom calendar.
26+
* Define joins from your cubes with facts to those calendar cubes, e.g., `base_orders`,
27+
and bring relevant calendar attributes as [proxy dimensions][ref-proxy-dimensions].
28+
29+
The last two steps require a few lines of code but it can totally be optimized with
30+
a [Jinja macro][ref-jinja-macro] if needed.
31+
32+
### Calendar table
33+
34+
Consider the following calendar cube. It was generated using a large language model
35+
(LLM) and then tested against the [official calendar][link-454-official-calendar].
36+
In this example, it's generated on the fly, however, in production, it should be
37+
materialized as a table using a data transformation tool:
38+
39+
```yaml
40+
cubes:
41+
- name: calendar_454
42+
public: false
43+
sql: >
44+
WITH RECURSIVE fiscal_weeks AS (
45+
-- Step 1: Define the start of the fiscal years (Sunday closest to Feb 1st)
46+
SELECT
47+
year AS fiscal_year,
48+
CASE
49+
WHEN strftime('%w', date_trunc('week', make_date(year, 2, 1)))::INTEGER <= 3
50+
THEN date_trunc('week', make_date(year, 2, 1)) + INTERVAL 6 DAY
51+
ELSE date_trunc('week', make_date(year, 2, 1) + INTERVAL 7 DAY) + INTERVAL 7 DAY
52+
END AS week_start,
53+
1 AS week_number,
54+
1 AS month_number,
55+
1 AS month_week_count
56+
FROM range(2015, 2031) t(year)
57+
58+
UNION ALL
59+
60+
-- Step 2: Generate weeks recursively following the 4-5-4 pattern
61+
SELECT
62+
fiscal_year,
63+
week_start + INTERVAL 7 DAY AS week_start,
64+
week_number + 1,
65+
CASE
66+
WHEN month_number = 12 AND ((month_week_count = 4 AND month_number % 3 = 1) OR
67+
(month_week_count = 5 AND month_number % 3 = 2) OR
68+
(month_week_count = 4 AND month_number % 3 = 0))
69+
THEN 1
70+
WHEN month_week_count = 4 AND (month_number % 3 = 1) THEN month_number + 1
71+
WHEN month_week_count = 5 AND (month_number % 3 = 2) THEN month_number + 1
72+
WHEN month_week_count = 4 AND (month_number % 3 = 0) THEN month_number + 1
73+
ELSE month_number
74+
END AS month_number,
75+
CASE
76+
WHEN month_week_count = 4 AND (month_number % 3 = 1) THEN 1
77+
WHEN month_week_count = 5 AND (month_number % 3 = 2) THEN 1
78+
WHEN month_week_count = 4 AND (month_number % 3 = 0) THEN 1
79+
ELSE month_week_count + 1
80+
END AS month_week_count
81+
FROM fiscal_weeks
82+
WHERE week_number < 52 OR (week_number = 52 AND (fiscal_year % 5 = 2)) -- Account for 53rd week
83+
)
84+
85+
SELECT
86+
fiscal_year,
87+
week_number,
88+
month_number,
89+
make_timestamp(fiscal_year, month_number, 1, 0, 0, 0) AS fiscal_month_date,
90+
week_start AS week_start_date,
91+
make_timestamp(year(week_start + INTERVAL 6 DAY),
92+
month(week_start + INTERVAL 6 DAY),
93+
day(week_start + INTERVAL 6 DAY),
94+
23, 59, 59.999) AS week_end_date
95+
FROM fiscal_weeks
96+
ORDER BY fiscal_year, week_number
97+
98+
dimensions:
99+
- name: retail_year
100+
sql: fiscal_year
101+
type: number
102+
103+
- name: week_number
104+
sql: week_number
105+
type: number
106+
107+
- name: month_number
108+
sql: month_number
109+
type: number
110+
111+
- name: retail_month_date
112+
sql: fiscal_month_date
113+
type: time
114+
115+
- name: week_start_date
116+
sql: week_start_date
117+
type: time
118+
119+
- name: week_end_date
120+
sql: week_end_date
121+
type: time
122+
```
123+
124+
As you can see, this cube defines `week_start_date` and `week_end_date` time dimensions
125+
as the start and end dates of the retail week. They can be used to join this cube to
126+
cubes with facts.
127+
128+
### Auxiliary calendar cubes
129+
130+
We will also extend the `calendar_454` cube to create auxiliary calendar cubes for
131+
three time dimensions that we'd like to translate to the 4-5-4 calendar:
132+
133+
```yaml
134+
cubes:
135+
- name: calendar_454__base_orders__created_at
136+
extends: calendar_454
137+
138+
- name: calendar_454__base_orders__completed_at
139+
extends: calendar_454
140+
```
141+
142+
### Cubes with facts
143+
144+
Finally, we define joins from the `base_orders` cube to the auxiliary calendar cubes.
145+
We also bring the `week_number` and `month_number` attributes as proxy dimensions:
146+
147+
```yaml
148+
cubes:
149+
- name: base_orders
150+
sql: SELECT * FROM 's3://cube-tutorial/orders.csv'
151+
152+
joins:
153+
# BEGIN — Joins to calendar tables
154+
- name: calendar_454__base_orders__created_at
155+
sql: "{CUBE.created_at} BETWEEN {calendar_454__base_orders__created_at.week_start_date} AND {calendar_454__base_orders__created_at.week_end_date}"
156+
relationship: many_to_one
157+
158+
- name: calendar_454__base_orders__completed_at
159+
sql: "{CUBE.completed_at} BETWEEN {calendar_454__base_orders__completed_at.week_start_date} AND {calendar_454__base_orders__completed_at.week_end_date}"
160+
relationship: many_to_one
161+
# END — Joins to calendar tables
162+
163+
dimensions:
164+
- name: id
165+
sql: id
166+
type: number
167+
primary_key: true
168+
169+
- name: status
170+
sql: status
171+
type: string
172+
173+
# BEGIN — Regular time dimension + ones derived from calendar table
174+
- name: created_at
175+
sql: "{CUBE}.created_at::TIMESTAMP"
176+
type: time
177+
178+
- name: created_at_retail_month
179+
sql: "{calendar_454__base_orders__created_at.retail_month_date}"
180+
type: time
181+
182+
- name: created_at_retail_week
183+
sql: "{calendar_454__base_orders__created_at.week_number}"
184+
type: number
185+
186+
- name: completed_at
187+
sql: "{CUBE}.completed_at::TIMESTAMP"
188+
type: time
189+
190+
- name: completed_at_retail_month
191+
sql: "{calendar_454__base_orders__completed_at.retail_month_date}"
192+
type: time
193+
194+
- name: completed_at_retail_week
195+
sql: "{calendar_454__base_orders__completed_at.week_number}"
196+
type: number
197+
# END — Regular time dimension + ones derived from calendar table
198+
199+
measures:
200+
- name: count
201+
type: count
202+
203+
- name: completed_count
204+
type: count
205+
filters:
206+
- sql: "{CUBE}.status = 'completed'"
207+
```
208+
209+
## Result
210+
211+
Querying this data modal would yield the following result:
212+
213+
<Screenshot src="https://ucarecdn.com/7d7d981c-8ed8-4438-865e-cbcda01c81d8/"/>
214+
215+
216+
[link-454]: https://nrf.com/resources/4-5-4-calendar
217+
[link-454-official-calendar]: https://2fb5c46100c1b71985e2-011e70369171d43105aff38e48482379.ssl.cf1.rackcdn.com/4-5-4%20calendar/3-Year-Calendar-5-27.pdf
218+
[ref-custom-granularities]: /reference/data-model/dimensions#granularities
219+
[ref-custom-granularities-recipe]: /guides/recipes/data-modeling/custom-granularity
220+
[ref-proxy-dimensions]: /product/data-modeling/concepts/calculated-members#proxy-dimensions
221+
[ref-jinja-macro]: /product/data-modeling/dynamic/jinja#macros

docs/pages/guides/recipes/data-modeling/custom-granularity.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Implementing custom time dimension granularities
22

3-
This recipe show examples of commonly used [custom
3+
This recipe shows examples of commonly used [custom
44
granularities][ref-custom-granularities].
55

66
## Use case

0 commit comments

Comments
 (0)