Skip to content
Good engineers know 3 min read · Dates 2 practice problems ↓

trunc / add_months

Truncate dates to the month or year and shift them by whole months, including month-end rules.

You will learn

  • How to truncate dates to the month, quarter or year
  • The difference between trunc and date_trunc
  • How add_months handles month ends
  • How to build month-over-month comparisons

Read first

Comfortable with these? Read on.

TL;DR F.trunc(date, "month") returns the first day of the month as a date; F.date_trunc("month", ts) does the same for timestamps. F.add_months(date, n) shifts by whole months, clamping to the last valid day.

What they do

FunctionInputExample result
trunc(d, "month")date2025-03-31 → 2025-03-01
trunc(d, "quarter")date2025-05-20 → 2025-04-01
trunc(d, "year")date2025-05-20 → 2025-01-01
date_trunc("hour", ts)timestamp10:47:12 → 10:00:00
add_months(d, 1)date2025-01-31 → 2025-02-28
last_day(d)date2025-02-10 → 2025-02-28

Note the argument order: trunc(column, unit) but date_trunc(unit, column). trunc returns a date and only knows date units; date_trunc returns a timestamp and also handles hours, minutes and weeks.

How add_months handles month ends

Adding a month to January 31 cannot give February 31. Spark returns the last day of the target month instead: February 28 (or 29 in a leap year). Going back is not symmetric: add_months("2025-02-28", 1) is March 28, not March 31.

Note: Spark 2 and Spark 3 differ here. Spark 2 kept "last day stays last day", so Feb 28 + 1 month gave Mar 31. Spark 3 adds the months and only clamps when the day does not exist.

Step by step: monthly revenue

Input

order_dateamount
2025-01-15100
2025-01-3140
2025-02-1070
2025-03-3090
2025-03-3160

Output

monthrevenue
2025-01-01140
2025-02-0170
2025-03-01150

Truncating to the month gives every order in a month the same key, ready for groupBy.

Run the example

PySparkSpark SQL
from pyspark.sql import functions as F
result = (orders
    .withColumn("month", F.trunc("order_date", "month"))
    .groupBy("month").agg(F.sum("amount").alias("revenue"))
    .withColumn("next_month", F.add_months("month", 1))
    .orderBy("month"))
SELECT trunc(order_date, 'month')                AS month,
       SUM(amount)                                 AS revenue,
       add_months(trunc(order_date, 'month'), 1)   AS next_month
FROM orders
GROUP BY trunc(order_date, 'month')
ORDER BY month

Switch to PySpark to edit and run this example in your browser.

Month over month

Two common ways to compare each month with the previous one: lag(revenue) over months ordered in time (fast, but skips months with no data), or join the monthly table to itself on a.month = add_months(b.month, 1) (correct even when months are missing, and clearer about which month is compared). For guaranteed rows for every month, generate a calendar with sequence(start, end, interval 1 month) and left join to it.

Parsing first

These functions need real dates. A string in ISO format (yyyy-MM-dd) is cast automatically; anything else must be parsed with F.to_date(col, "dd/MM/yyyy"). In Spark 3, an unparseable string returns null; with ANSI mode in Spark 4.0, to_date raises an error unless you use try_to_date.

Common mistakes

Swapping the arguments of trunc and date_trunc

trunc(col, unit) vs date_trunc(unit, col). Swapped arguments return null or fail.

Grouping by month(date) alone

month() returns 1 to 12, so January 2024 and January 2025 merge. Truncate instead.

Using lag for month over month with gaps

If a month has no rows, lag compares with the month before it.

Key takeaways

  • trunc(date, unit) gives the start of the month, quarter or year.
  • date_trunc(unit, ts) does the same for timestamps, with finer units.
  • add_months clamps to the last valid day; it is not symmetric.
  • Group by the truncated date, not month(), to keep years apart.

Check yourself

3 questions

1. What is add_months('2025-01-31', 1)?

Show the answer

2025-02-28. February 31 does not exist, so Spark clamps to the last day of February.

2. Why is grouping by F.month("order_date") risky?

Show the answer

It returns 1 to 12, merging the same month of different years. month() drops the year. Truncate to the month to keep a full date key.

3. Which call is correct for a timestamp column ts?

Show the answer

F.date_trunc("hour", "ts"). date_trunc takes the unit first and supports hours. trunc takes the column first and only date units.

Practice it

Interview problems that use trunc / add_months: write the PySpark, run it, and get graded on hidden tests.

Solve: Month Start, Month End and Due Dates →

Go deeper

Primary sources: functions.trunc · functions.add_months · Datetime patterns