Repository: duckdb/duckdb
Pull request #16889: Rework ICU age computation to convert to a timestamp and use the regular interval age computation
Author: login: Mytherin
Created: 2025-03-29T08:54:24Z | Merged: 2025-03-30T13:24:02Z

Effectively we switch to decomposing the timestamp into its base units (year/month/day/hour/minute/second) - and then converting those units back into a timestamp, followed by performing the interval age computation.

This greatly speeds up the ICU age computation, and also aligns our results with that of Postgres.

Previously:
```sql
D set timezone='UTC';
D select age(timestamptz '2001-04-10', TIMESTAMPTZ '1957-06-13') as res;
┌───────────────────────────┐
│            res            │
│         interval          │
├───────────────────────────┤
│ 43 years 9 months 28 days │
└───────────────────────────┘
```

Current behavior (and Postgres behavior):

```sql
postgres=# set timezone='UTC';
SET
postgres=# select age(timestamptz '2001-04-10', TIMESTAMPTZ '1957-06-13') as res;
           res           
-------------------------
 43 years 9 mons 27 days
(1 row)

```




Diff:
diff --git a/extension/icu/icu-dateadd.cpp b/extension/icu/icu-dateadd.cpp
index 29b0cf73bc2d..32fe9ad1ada7 100644
--- a/extension/icu/icu-dateadd.cpp
+++ b/extension/icu/icu-dateadd.cpp
@@ -8,26 +8,27 @@
 #include "duckdb/parser/parsed_data/create_scalar_function_info.hpp"
 #include "duckdb/planner/expression/bound_function_expression.hpp"
 #include "include/icu-datefunc.hpp"
+#include "icu-helpers.hpp"
 
 namespace duckdb {
 
 struct ICUCalendarAdd {
 	template <class TA, class TB, class TR>
-	static inline TR Operation(TA left, TB right, icu::Calendar *calendar) {
+	static inline TR Operation(TA left, TB right, TZCalendar &calendar_p) {
 		throw InternalException("Unimplemented type for ICUCalendarAdd");
 	}
 };
 
 struct ICUCalendarSub : public ICUDateFunc {
 	template <class TA, class TB, class TR>
-	static inline TR Operation(TA left, TB right, icu::Calendar *calendar) {
+	static inline TR Operation(TA left, TB right, TZCalendar &calendar_p) {
 		throw InternalException("Unimplemented type for ICUCalendarSub");
 	}
 };
 
 struct ICUCalendarAge : public ICUDateFunc {
 	template <class TA, class TB, class TR>
-	static inline TR Operation(TA left, TB right, icu::Calendar *calendar) {
+	static inline TR Operation(TA left, TB right, TZCalendar &calendar_p) {
 		throw InternalException("Unimplemented type for ICUCalendarAge");
 	}
 };
@@ -53,10 +54,11 @@ static inline void CalendarAddHour(icu::Calendar *calendar, int64_t interval_hou
 }
 
 template <>
-timestamp_t ICUCalendarAdd::Operation(timestamp_t timestamp, interval_t interval, icu::Calendar *calendar) {
+timestamp_t ICUCalendarAdd::Operation(timestamp_t timestamp, interval_t interval, TZCalendar &calendar_p) {
 	if (!Timestamp::IsFinite(timestamp)) {
 		return timestamp;
 	}
+	auto calendar = calendar_p.GetICUCalendar();
 
 	int64_t millis = timestamp.value / Interval::MICROS_PER_MSEC;
 	int64_t micros = timestamp.value % Interval::MICROS_PER_MSEC;
@@ -120,25 +122,26 @@ timestamp_t ICUCalendarAdd::Operation(timestamp_t timestamp, interval_t interval
 }
 
 template <>
-timestamp_t ICUCalendarAdd::Operation(interval_t interval, timestamp_t timestamp, icu::Calendar *calendar) {
+timestamp_t ICUCalendarAdd::Operation(interval_t interval, timestamp_t timestamp, TZCalendar &calendar) {
 	return Operation<timestamp_t, interval_t, timestamp_t>(timestamp, interval, calendar);
 }
 
 template <>
-timestamp_t ICUCalendarSub::Operation(timestamp_t timestamp, interval_t interval, icu::Calendar *calendar) {
+timestamp_t ICUCalendarSub::Operation(timestamp_t timestamp, interval_t interval, TZCalendar &calendar) {
 	const interval_t negated {-interval.months, -interval.days, -interval.micros};
 	return ICUCalendarAdd::template Operation<timestamp_t, interval_t, timestamp_t>(timestamp, negated, calendar);
 }
 
 template <>
-interval_t ICUCalendarSub::Operation(timestamp_t end_date, timestamp_t start_date, icu::Calendar *calendar) {
+interval_t ICUCalendarSub::Operation(timestamp_t end_date, timestamp_t start_date, TZCalendar &calendar_p) {
 	if (!Timestamp::IsFinite(end_date) || !Timestamp::IsFinite(start_date)) {
 		throw InvalidInputException("Cannot subtract infinite timestamps");
 	}
 	if (start_date > end_date) {
-		auto negated = Operation<timestamp_t, timestamp_t, interval_t>(start_date, end_date, calendar);
+		auto negated = Operation<timestamp_t, timestamp_t, interval_t>(start_date, end_date, calendar_p);
 		return {-negated.months, -negated.days, -negated.micros};
 	}
+	auto calendar = calendar_p.GetICUCalendar();
 
 	auto start_micros = ICUDateFunc::SetTime(calendar, start_date);
 	auto end_micros = (uint64_t)(end_date.value % Interval::MICROS_PER_MSEC);
@@ -166,9 +169,16 @@ interval_t ICUCalendarSub::Operation(timestamp_t end_date, timestamp_t start_dat
 }
 
 template <>
-interval_t ICUCalendarAge::Operation(timestamp_t end_date, timestamp_t start_date, icu::Calendar *calendar) {
+interval_t ICUCalendarAge::Operation(timestamp_t end_date, timestamp_t start_date, TZCalendar &calendar_p) {
+	auto calendar = calendar_p.GetICUCalendar();
+	if (calendar_p.IsGregorian()) {
+		auto start_data = ICUHelpers::GetComponents(timestamp_tz_t(start_date.value), calendar);
+		auto end_data = ICUHelpers::GetComponents(timestamp_tz_t(end_date.value), calendar);
+		return Interval::GetAge(end_data, start_data, start_date > end_date);
+	}
+	// fallback for non-gregorian calendars, since Interval::GetAge does not handle
 	if (start_date > end_date) {
-		auto negated = Operation<timestamp_t, timestamp_t, interval_t>(start_date, end_date, calendar);
+		auto negated = Operation<timestamp_t, timestamp_t, interval_t>(start_date, end_date, calendar_p);
 		return {-negated.months, -negated.days, -negated.micros};
 	}
 
@@ -205,13 +215,13 @@ struct ICUDateAdd : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar(info.calendar->clone());
+		TZCalendar calendar(*info.calendar, info.cal_setting);
 
 		//	Subtract argument from current_date (at midnight)
-		const auto end_date = CurrentMidnight(calendar.get(), state);
+		const auto end_date = CurrentMidnight(calendar.GetICUCalendar(), state);
 
 		UnaryExecutor::Execute<TA, TR>(args.data[0], result, args.size(), [&](TA start_date) {
-			return OP::template Operation<timestamp_t, TA, TR>(end_date, start_date, calendar.get());
+			return OP::template Operation<timestamp_t, TA, TR>(end_date, start_date, calendar);
 		});
 	}
 
@@ -227,10 +237,10 @@ struct ICUDateAdd : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar(info.calendar->clone());
+		TZCalendar calendar(*info.calendar, info.cal_setting);
 
 		BinaryExecutor::Execute<TA, TB, TR>(args.data[0], args.data[1], result, args.size(), [&](TA left, TB right) {
-			return OP::template Operation<TA, TB, TR>(left, right, calendar.get());
+			return OP::template Operation<TA, TB, TR>(left, right, calendar);
 		});
 	}
 
@@ -287,15 +297,15 @@ struct ICUDateAdd : public ICUDateFunc {
 	}
 };
 
-timestamp_t ICUDateFunc::Add(icu::Calendar *calendar, timestamp_t timestamp, interval_t interval) {
+timestamp_t ICUDateFunc::Add(TZCalendar &calendar, timestamp_t timestamp, interval_t interval) {
 	return ICUCalendarAdd::Operation<timestamp_t, interval_t, timestamp_t>(timestamp, interval, calendar);
 }
 
-timestamp_t ICUDateFunc::Sub(icu::Calendar *calendar, timestamp_t timestamp, interval_t interval) {
+timestamp_t ICUDateFunc::Sub(TZCalendar &calendar, timestamp_t timestamp, interval_t interval) {
 	return ICUCalendarSub::Operation<timestamp_t, interval_t, timestamp_t>(timestamp, interval, calendar);
 }
 
-interval_t ICUDateFunc::Sub(icu::Calendar *calendar, timestamp_t end_date, timestamp_t start_date) {
+interval_t ICUDateFunc::Sub(TZCalendar &calendar, timestamp_t end_date, timestamp_t start_date) {
 	return ICUCalendarSub::Operation<timestamp_t, timestamp_t, interval_t>(end_date, start_date, calendar);
 }
 
diff --git a/extension/icu/icu-list-range.cpp b/extension/icu/icu-list-range.cpp
index ea50d7be3205..490627954a8d 100644
--- a/extension/icu/icu-list-range.cpp
+++ b/extension/icu/icu-list-range.cpp
@@ -60,7 +60,7 @@ struct ICUListRange : public ICUDateFunc {
 			increment_value = ListIncrementValue(row_idx);
 		}
 
-		uint64_t ListLength(idx_t row_idx, icu::Calendar *calendar) {
+		uint64_t ListLength(idx_t row_idx, TZCalendar &calendar) {
 			timestamp_t start_value;
 			timestamp_t end_value;
 			interval_t increment_value;
@@ -68,7 +68,7 @@ struct ICUListRange : public ICUDateFunc {
 			return ListLength(start_value, end_value, increment_value, INCLUSIVE_BOUND, calendar);
 		}
 
-		void Increment(timestamp_t &input, interval_t increment, icu::Calendar *calendar) {
+		void Increment(timestamp_t &input, interval_t increment, TZCalendar &calendar) {
 			input = Add(calendar, input, increment);
 		}
 
@@ -77,7 +77,7 @@ struct ICUListRange : public ICUDateFunc {
 		UnifiedVectorFormat vdata[3];
 
 		uint64_t ListLength(timestamp_t start_value, timestamp_t end_value, interval_t increment_value,
-		                    bool inclusive_bound, icu::Calendar *calendar) {
+		                    bool inclusive_bound, TZCalendar &calendar) {
 			bool is_positive = increment_value.months > 0 || increment_value.days > 0 || increment_value.micros > 0;
 			bool is_negative = increment_value.months < 0 || increment_value.days < 0 || increment_value.micros < 0;
 			if (!is_negative && !is_positive) {
@@ -130,8 +130,7 @@ struct ICUListRange : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &bind_info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar_ptr(bind_info.calendar->clone());
-		auto calendar = calendar_ptr.get();
+		TZCalendar calendar(*bind_info.calendar, bind_info.cal_setting);
 
 		RangeInfoStruct<INCLUSIVE_BOUND> info(args);
 		idx_t args_size = 1;
diff --git a/extension/icu/icu-strptime.cpp b/extension/icu/icu-strptime.cpp
index 298b0b2e5ff2..696b63df70eb 100644
--- a/extension/icu/icu-strptime.cpp
+++ b/extension/icu/icu-strptime.cpp
@@ -1,5 +1,6 @@
 #include "include/icu-strptime.hpp"
 #include "include/icu-datefunc.hpp"
+#include "include/icu-helpers.hpp"
 
 #include "duckdb/catalog/catalog_entry/scalar_function_catalog_entry.hpp"
 #include "duckdb/common/operator/cast_operators.hpp"
@@ -17,6 +18,28 @@
 
 namespace duckdb {
 
+TimestampComponents ICUHelpers::GetComponents(timestamp_tz_t ts, icu::Calendar *calendar) {
+	// Get the parts in the given time zone
+	uint64_t micros = ICUDateFunc::SetTime(calendar, timestamp_t(ts.value));
+
+	TimestampComponents ts_data;
+	ts_data.year = ICUDateFunc::ExtractField(calendar, UCAL_EXTENDED_YEAR);
+	ts_data.month = ICUDateFunc::ExtractField(calendar, UCAL_MONTH) + 1;
+	ts_data.day = ICUDateFunc::ExtractField(calendar, UCAL_DATE);
+
+	ts_data.hour = ICUDateFunc::ExtractField(calendar, UCAL_HOUR_OF_DAY);
+	ts_data.minute = ICUDateFunc::ExtractField(calendar, UCAL_MINUTE);
+	ts_data.second = ICUDateFunc::ExtractField(calendar, UCAL_SECOND);
+	ts_data.microsecond = UnsafeNumericCast<int32_t>(
+	    ICUDateFunc::ExtractField(calendar, UCAL_MILLISECOND) * Interval::MICROS_PER_MSEC + micros);
+	return ts_data;
+}
+
+timestamp_t ICUHelpers::ToTimestamp(TimestampComponents data) {
+	date_t date_val = Date::FromDate(data.year, data.month, data.day);
+	dtime_t time_val = Time::FromTime(data.hour, data.minute, data.second, data.microsecond);
+	return Timestamp::FromDatetime(date_val, time_val);
+}
 struct ICUStrptime : public ICUDateFunc {
 	using ParseResult = StrpTimeFormat::ParseResult;
 
@@ -445,27 +468,15 @@ struct ICUStrftime : public ICUDateFunc {
 			return StringVector::AddString(result, Timestamp::ToString(input));
 		}
 
-		// Get the parts in the given time zone
-		uint64_t micros = SetTime(calendar, input);
-
-		int32_t date_units[3];
-		date_units[0] = ExtractField(calendar, UCAL_EXTENDED_YEAR); // strftime doesn't understand eras.
-		date_units[1] = ExtractField(calendar, UCAL_MONTH) + 1;
-		date_units[2] = ExtractField(calendar, UCAL_DATE);
-
-		int32_t time_units[4];
-		time_units[0] = ExtractField(calendar, UCAL_HOUR_OF_DAY);
-		time_units[1] = ExtractField(calendar, UCAL_MINUTE);
-		time_units[2] = ExtractField(calendar, UCAL_SECOND);
-		time_units[3] =
-		    UnsafeNumericCast<int32_t>(ExtractField(calendar, UCAL_MILLISECOND) * Interval::MICROS_PER_MSEC + micros);
+		// decompose the timestamp
+		auto ts_data = ICUHelpers::GetComponents(timestamp_tz_t(input.value), calendar);
 
 		idx_t year_length;
 		bool add_bc;
-		const auto date_len = DateToStringCast::Length(date_units, year_length, add_bc);
+		const auto date_len = DateToStringCast::YearLength(ts_data.year, year_length, add_bc);
 
 		char micro_buffer[6];
-		const auto time_len = TimeToStringCast::Length(time_units, micro_buffer);
+		const auto time_len = TimeToStringCast::MicrosLength(ts_data.microsecond, micro_buffer);
 
 		auto offset = ExtractField(calendar, UCAL_ZONE_OFFSET) + ExtractField(calendar, UCAL_DST_OFFSET);
 		offset /= Interval::MSECS_PER_SEC;
@@ -479,11 +490,12 @@ struct ICUStrftime : public ICUDateFunc {
 		string_t target = StringVector::EmptyString(result, len);
 		auto buffer = target.GetDataWriteable();
 
-		DateToStringCast::Format(buffer, date_units, year_length, add_bc);
+		DateToStringCast::Format(buffer, ts_data.year, ts_data.month, ts_data.day, year_length, add_bc);
 		buffer += date_len;
 		*buffer++ = ' ';
 
-		TimeToStringCast::Format(buffer, time_len, time_units, micro_buffer);
+		TimeToStringCast::Format(buffer, time_len, ts_data.hour, ts_data.minute, ts_data.second, ts_data.microsecond,
+		                         micro_buffer);
 		buffer += time_len;
 
 		memcpy(buffer, offset_str.c_str(), offset_len);
diff --git a/extension/icu/icu-table-range.cpp b/extension/icu/icu-table-range.cpp
index 941b3c5245c2..70572ba3c197 100644
--- a/extension/icu/icu-table-range.cpp
+++ b/extension/icu/icu-table-range.cpp
@@ -7,6 +7,7 @@
 #include "duckdb/parser/parsed_data/create_table_function_info.hpp"
 #include "include/icu-datefunc.hpp"
 #include "unicode/calendar.h"
+#include "tz_calendar.hpp"
 
 namespace duckdb {
 
@@ -151,8 +152,7 @@ struct ICUTableRange {
 	                                                DataChunk &input, DataChunk &output) {
 		auto &bind_data = data_p.bind_data->Cast<ICURangeBindData>();
 		auto &state = data_p.local_state->Cast<ICURangeLocalState>();
-		CalendarPtr calendar_ptr(bind_data.calendar->clone());
-		auto calendar = calendar_ptr.get();
+		TZCalendar calendar(*bind_data.calendar, bind_data.cal_setting);
 		while (true) {
 			if (!state.initialized_row) {
 				// initialize for the current input row
diff --git a/extension/icu/icu-timebucket.cpp b/extension/icu/icu-timebucket.cpp
index ffb62ca1bff9..8db9012c94bd 100644
--- a/extension/icu/icu-timebucket.cpp
+++ b/extension/icu/icu-timebucket.cpp
@@ -62,7 +62,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 	}
 
 	static inline timestamp_t WidthConvertibleToMicrosCommon(int64_t bucket_width_micros, const timestamp_t ts,
-	                                                         const timestamp_t origin, icu::Calendar *calendar) {
+	                                                         const timestamp_t origin, TZCalendar &calendar) {
 		if (!bucket_width_micros) {
 			throw OutOfRangeException("Can't bucket using zero microseconds");
 		}
@@ -78,13 +78,13 @@ struct ICUTimeBucket : public ICUDateFunc {
 	}
 
 	static inline timestamp_t WidthConvertibleToDaysCommon(int32_t bucket_width_days, const timestamp_t ts,
-	                                                       const timestamp_t origin, icu::Calendar *calendar) {
+	                                                       const timestamp_t origin, TZCalendar &calendar) {
 		if (!bucket_width_days) {
 			throw OutOfRangeException("Can't bucket using zero days");
 		}
 		const auto sub_days = SubtractFactory(DatePartSpecifier::DAY);
 
-		int64_t ts_days = sub_days(calendar, origin, ts);
+		int64_t ts_days = sub_days(calendar.GetICUCalendar(), origin, ts);
 		int64_t result_days = (ts_days / bucket_width_days) * bucket_width_days;
 		if (result_days < NumericLimits<int32_t>::Minimum() || result_days > NumericLimits<int32_t>::Maximum()) {
 			throw OutOfRangeException("Timestamp out of range");
@@ -100,13 +100,14 @@ struct ICUTimeBucket : public ICUDateFunc {
 	}
 
 	static inline timestamp_t WidthConvertibleToMonthsCommon(int32_t bucket_width_months, const timestamp_t ts,
-	                                                         const timestamp_t origin, icu::Calendar *calendar) {
+	                                                         const timestamp_t origin, TZCalendar &calendar_p) {
 		if (!bucket_width_months) {
 			throw OutOfRangeException("Can't bucket using zero months");
 		}
 		const auto trunc_months = TruncationFactory(DatePartSpecifier::MONTH);
 		const auto sub_months = SubtractFactory(DatePartSpecifier::MONTH);
 
+		auto calendar = calendar_p.GetICUCalendar();
 		uint64_t tmp_micros = SetTime(calendar, ts);
 		trunc_months(calendar, tmp_micros);
 		timestamp_t truncated_ts = GetTimeUnsafe(calendar, tmp_micros);
@@ -126,7 +127,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 			    SubtractOperatorOverflowCheck::Operation<int32_t, int32_t, int32_t>(result_months, bucket_width_months);
 		}
 
-		return Add(calendar, truncated_origin, interval_t {static_cast<int32_t>(result_months), 0, 0});
+		return Add(calendar_p, truncated_origin, interval_t {static_cast<int32_t>(result_months), 0, 0});
 	}
 
 	template <typename TA, typename TB, typename TR, typename OP>
@@ -157,7 +158,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 	}
 
 	struct WidthConvertibleToMicrosBinaryOperator {
-		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, icu::Calendar *calendar) {
+		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -167,7 +168,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 	};
 
 	struct WidthConvertibleToDaysBinaryOperator {
-		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, icu::Calendar *calendar) {
+		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -177,7 +178,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 	};
 
 	struct WidthConvertibleToMonthsBinaryOperator {
-		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, icu::Calendar *calendar) {
+		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -187,7 +188,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 	};
 
 	struct BinaryOperator {
-		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, icu::Calendar *calendar) {
+		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, TZCalendar &calendar) {
 			BucketWidthType bucket_width_type = ClassifyBucketWidthErrorThrow(bucket_width);
 			switch (bucket_width_type) {
 			case BucketWidthType::CONVERTIBLE_TO_MICROS:
@@ -204,7 +205,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OffsetWidthConvertibleToMicrosTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, interval_t offset,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -217,7 +218,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OffsetWidthConvertibleToDaysTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, interval_t offset,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -230,7 +231,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OffsetWidthConvertibleToMonthsTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, interval_t offset,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -243,7 +244,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OffsetTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, interval_t offset,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			BucketWidthType bucket_width_type = ClassifyBucketWidthErrorThrow(bucket_width);
 			switch (bucket_width_type) {
 			case BucketWidthType::CONVERTIBLE_TO_MICROS:
@@ -260,7 +261,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OriginWidthConvertibleToMicrosTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -270,7 +271,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OriginWidthConvertibleToDaysTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -280,7 +281,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OriginWidthConvertibleToMonthsTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -290,7 +291,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct OriginTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    ValidityMask &mask, idx_t idx, icu::Calendar *calendar) {
+		                                    ValidityMask &mask, idx_t idx, TZCalendar &calendar) {
 			if (!Value::IsFinite(origin)) {
 				mask.SetInvalid(idx);
 				return timestamp_t(0);
@@ -311,7 +312,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct TimeZoneWidthConvertibleToMicrosBinaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -321,7 +322,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct TimeZoneWidthConvertibleToDaysBinaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -331,7 +332,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct TimeZoneWidthConvertibleToMonthsBinaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, timestamp_t origin,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar) {
 			if (!Value::IsFinite(ts)) {
 				return ts;
 			}
@@ -341,7 +342,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 	struct TimeZoneTernaryOperator {
 		static inline timestamp_t Operation(interval_t bucket_width, timestamp_t ts, string_t tz,
-		                                    icu::Calendar *calendar) {
+		                                    TZCalendar &calendar_p) {
+			auto calendar = calendar_p.GetICUCalendar();
 			SetTimeZone(calendar, tz);
 
 			timestamp_t origin;
@@ -349,13 +351,13 @@ struct ICUTimeBucket : public ICUDateFunc {
 			switch (bucket_width_type) {
 			case BucketWidthType::CONVERTIBLE_TO_MICROS:
 				origin = ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
-				return TimeZoneWidthConvertibleToMicrosBinaryOperator::Operation(bucket_width, ts, origin, calendar);
+				return TimeZoneWidthConvertibleToMicrosBinaryOperator::Operation(bucket_width, ts, origin, calendar_p);
 			case BucketWidthType::CONVERTIBLE_TO_DAYS:
 				origin = ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
-				return TimeZoneWidthConvertibleToDaysBinaryOperator::Operation(bucket_width, ts, origin, calendar);
+				return TimeZoneWidthConvertibleToDaysBinaryOperator::Operation(bucket_width, ts, origin, calendar_p);
 			case BucketWidthType::CONVERTIBLE_TO_MONTHS:
 				origin = ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_2));
-				return TimeZoneWidthConvertibleToMonthsBinaryOperator::Operation(bucket_width, ts, origin, calendar);
+				return TimeZoneWidthConvertibleToMonthsBinaryOperator::Operation(bucket_width, ts, origin, calendar_p);
 			default:
 				throw NotImplementedException("Bucket type not implemented for ICU TIME_BUCKET");
 			}
@@ -367,9 +369,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar_ptr(info.calendar->clone());
-		auto calendar = calendar_ptr.get();
-		SetTimeZone(calendar, string_t("UTC"));
+		TZCalendar calendar(*info.calendar, info.cal_setting);
+		SetTimeZone(calendar.GetICUCalendar(), string_t("UTC"));
 
 		auto &bucket_width_arg = args.data[0];
 		auto &ts_arg = args.data[1];
@@ -423,9 +424,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar_ptr(info.calendar->clone());
-		auto calendar = calendar_ptr.get();
-		SetTimeZone(calendar, string_t("UTC"));
+		TZCalendar calendar(*info.calendar, info.cal_setting);
+		SetTimeZone(calendar.GetICUCalendar(), string_t("UTC"));
 
 		auto &bucket_width_arg = args.data[0];
 		auto &ts_arg = args.data[1];
@@ -488,9 +488,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar_ptr(info.calendar->clone());
-		auto calendar = calendar_ptr.get();
-		SetTimeZone(calendar, string_t("UTC"));
+		TZCalendar calendar(*info.calendar, info.cal_setting);
+		SetTimeZone(calendar.GetICUCalendar(), string_t("UTC"));
 
 		auto &bucket_width_arg = args.data[0];
 		auto &ts_arg = args.data[1];
@@ -556,8 +555,7 @@ struct ICUTimeBucket : public ICUDateFunc {
 
 		auto &func_expr = state.expr.Cast<BoundFunctionExpression>();
 		auto &info = func_expr.bind_info->Cast<BindData>();
-		CalendarPtr calendar_ptr(info.calendar->clone());
-		auto calendar = calendar_ptr.get();
+		TZCalendar calendar(*info.calendar, info.cal_setting);
 
 		auto &bucket_width_arg = args.data[0];
 		auto &ts_arg = args.data[1];
@@ -570,13 +568,13 @@ struct ICUTimeBucket : public ICUDateFunc {
 				ConstantVector::SetNull(result, true);
 			} else {
 				interval_t bucket_width = *ConstantVector::GetData<interval_t>(bucket_width_arg);
-				SetTimeZone(calendar, *ConstantVector::GetData<string_t>(tz_arg));
+				SetTimeZone(calendar.GetICUCalendar(), *ConstantVector::GetData<string_t>(tz_arg));
 				timestamp_t origin;
 				BucketWidthType bucket_width_type = ClassifyBucketWidth(bucket_width);
 				switch (bucket_width_type) {
 				case BucketWidthType::CONVERTIBLE_TO_MICROS:
-					origin =
-					    ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
+					origin = ICUDateFunc::FromNaive(calendar.GetICUCalendar(),
+					                                Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
 					BinaryExecutor::Execute<interval_t, timestamp_t, timestamp_t>(
 					    bucket_width_arg, ts_arg, result, args.size(), [&](interval_t bucket_width, timestamp_t ts) {
 						    return TimeZoneWidthConvertibleToMicrosBinaryOperator::Operation(bucket_width, ts, origin,
@@ -584,8 +582,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 					    });
 					break;
 				case BucketWidthType::CONVERTIBLE_TO_DAYS:
-					origin =
-					    ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
+					origin = ICUDateFunc::FromNaive(calendar.GetICUCalendar(),
+					                                Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_1));
 					BinaryExecutor::Execute<interval_t, timestamp_t, timestamp_t>(
 					    bucket_width_arg, ts_arg, result, args.size(), [&](interval_t bucket_width, timestamp_t ts) {
 						    return TimeZoneWidthConvertibleToDaysBinaryOperator::Operation(bucket_width, ts, origin,
@@ -593,8 +591,8 @@ struct ICUTimeBucket : public ICUDateFunc {
 					    });
 					break;
 				case BucketWidthType::CONVERTIBLE_TO_MONTHS:
-					origin =
-					    ICUDateFunc::FromNaive(calendar, Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_2));
+					origin = ICUDateFunc::FromNaive(calendar.GetICUCalendar(),
+					                                Timestamp::FromEpochMicroSeconds(DEFAULT_ORIGIN_MICROS_2));
 					BinaryExecutor::Execute<interval_t, timestamp_t, timestamp_t>(
 					    bucket_width_arg, ts_arg, result, args.size(), [&](interval_t bucket_width, timestamp_t ts) {
 						    return TimeZoneWidthConvertibleToMonthsBinaryOperator::Operation(bucket_width, ts, origin,
diff --git a/extension/icu/include/icu-datefunc.hpp b/extension/icu/include/icu-datefunc.hpp
index cc14203242f7..13c7be47e054 100644
--- a/extension/icu/include/icu-datefunc.hpp
+++ b/extension/icu/include/icu-datefunc.hpp
@@ -13,12 +13,11 @@
 #include "duckdb/common/enums/date_part_specifier.hpp"
 #include "duckdb/planner/expression/bound_function_expression.hpp"
 #include "unicode/calendar.h"
+#include "tz_calendar.hpp"
 
 namespace duckdb {
 
 struct ICUDateFunc {
-	using CalendarPtr = duckdb::unique_ptr<icu::Calendar>;
-
 	struct BindData : public FunctionData {
 		explicit BindData(ClientContext &context);
 		BindData(const string &tz_setting, const string &cal_setting);
@@ -66,11 +65,11 @@ struct ICUDateFunc {
 	//! Subtracts the field of the given date from the calendar
 	static int32_t SubtractField(icu::Calendar *calendar, UCalendarDateFields field, timestamp_t end_date);
 	//! Adds the timestamp and the interval using the calendar
-	static timestamp_t Add(icu::Calendar *calendar, timestamp_t timestamp, interval_t interval);
+	static timestamp_t Add(TZCalendar &calendar, timestamp_t timestamp, interval_t interval);
 	//! Subtracts the interval from the timestamp using the calendar
-	static timestamp_t Sub(icu::Calendar *calendar, timestamp_t timestamp, interval_t interval);
+	static timestamp_t Sub(TZCalendar &calendar, timestamp_t timestamp, interval_t interval);
 	//! Subtracts the latter timestamp from the former timestamp using the calendar
-	static interval_t Sub(icu::Calendar *calendar, timestamp_t end_date, timestamp_t start_date);
+	static interval_t Sub(TZCalendar &calendar, timestamp_t end_date, timestamp_t start_date);
 	//! Pulls out the bin values from the timestamp assuming it is an instant,
 	//! constructs an ICU timestamp, and then converts that back to a DuckDB instant
 	//! Adding offset doesn't really work around DST because the bin values are ambiguous
diff --git a/extension/icu/include/icu-helpers.hpp b/extension/icu/include/icu-helpers.hpp
index 788d191b7ed6..57889259048d 100644
--- a/extension/icu/include/icu-helpers.hpp
+++ b/extension/icu/include/icu-helpers.hpp
@@ -10,6 +10,7 @@
 
 #include "duckdb.hpp"
 #include "unicode/timezone.h"
+#include "duckdb/common/types/timestamp.hpp"
 
 namespace duckdb {
 
@@ -18,6 +19,10 @@ struct ICUHelpers {
 	static unique_ptr<icu::TimeZone> TryGetTimeZone(string &tz_str);
 	//! Gets a time zone - throws an error if the timezone is not found
 	static unique_ptr<icu::TimeZone> GetTimeZone(string &tz_str);
+
+	static TimestampComponents GetComponents(timestamp_tz_t ts, icu::Calendar *calendar);
+
+	static timestamp_t ToTimestamp(TimestampComponents data);
 };
 
 } // namespace duckdb
diff --git a/extension/icu/include/tz_calendar.hpp b/extension/icu/include/tz_calendar.hpp
new file mode 100644
index 000000000000..e0c306b846be
--- /dev/null
+++ b/extension/icu/include/tz_calendar.hpp
@@ -0,0 +1,35 @@
+//===----------------------------------------------------------------------===//
+//                         DuckDB
+//
+// tz_calendar.hpp
+//
+//
+//===----------------------------------------------------------------------===//
+
+#pragma once
+
+#include "icu-datefunc.hpp"
+#include "duckdb/common/string_util.hpp"
+
+namespace duckdb {
+
+using CalendarPtr = duckdb::unique_ptr<icu::Calendar>;
+
+struct TZCalendar {
+	TZCalendar(icu::Calendar &calendar_p, const string &cal_setting)
+	    : calendar(CalendarPtr(calendar_p.clone())),
+	      is_gregorian(cal_setting.empty() || StringUtil::CIEquals(cal_setting, "gregorian")) {
+	}
+
+	icu::Calendar *GetICUCalendar() {
+		return calendar.get();
+	}
+	bool IsGregorian() const {
+		return is_gregorian;
+	}
+
+	CalendarPtr calendar;
+	bool is_gregorian;
+};
+
+} // namespace duckdb
diff --git a/src/common/types/interval.cpp b/src/common/types/interval.cpp
index 719e9ee2c08e..e900b24021e7 100644
--- a/src/common/types/interval.cpp
+++ b/src/common/types/interval.cpp
@@ -342,38 +342,20 @@ int64_t Interval::GetNanoseconds(const interval_t &val) {
 	return nano;
 }
 
-interval_t Interval::GetAge(timestamp_t timestamp_1, timestamp_t timestamp_2) {
-	D_ASSERT(Timestamp::IsFinite(timestamp_1) && Timestamp::IsFinite(timestamp_2));
-	date_t date1, date2;
-	dtime_t time1, time2;
-
-	Timestamp::Convert(timestamp_1, date1, time1);
-	Timestamp::Convert(timestamp_2, date2, time2);
-
-	// and from date extract the years, months and days
-	int32_t year1, month1, day1;
-	int32_t year2, month2, day2;
-	Date::Convert(date1, year1, month1, day1);
-	Date::Convert(date2, year2, month2, day2);
-	// finally perform the differences
-	auto year_diff = year1 - year2;
-	auto month_diff = month1 - month2;
-	auto day_diff = day1 - day2;
-
-	// and from time extract hours, minutes, seconds and milliseconds
-	int32_t hour1, min1, sec1, micros1;
-	int32_t hour2, min2, sec2, micros2;
-	Time::Convert(time1, hour1, min1, sec1, micros1);
-	Time::Convert(time2, hour2, min2, sec2, micros2);
-	// finally perform the differences
-	auto hour_diff = hour1 - hour2;
-	auto min_diff = min1 - min2;
-	auto sec_diff = sec1 - sec2;
-	auto micros_diff = micros1 - micros2;
+interval_t Interval::GetAge(TimestampComponents ts1, TimestampComponents ts2, bool is_negative) {
+	// perform the differences
+	auto year_diff = ts1.year - ts2.year;
+	auto month_diff = ts1.month - ts2.month;
+	auto day_diff = ts1.day - ts2.day;
+
+	auto hour_diff = ts1.hour - ts2.hour;
+	auto min_diff = ts1.minute - ts2.minute;
+	auto sec_diff = ts1.second - ts2.second;
+	auto micros_diff = ts1.microsecond - ts2.microsecond;
 
 	// flip sign if necessary
 	bool sign_flipped = false;
-	if (timestamp_1 < timestamp_2) {
+	if (is_negative) {
 		year_diff = -year_diff;
 		month_diff = -month_diff;
 		day_diff = -day_diff;
@@ -401,11 +383,11 @@ interval_t Interval::GetAge(timestamp_t timestamp_1, timestamp_t timestamp_2) {
 		day_diff--;
 	}
 	while (day_diff < 0) {
-		if (timestamp_1 < timestamp_2) {
-			day_diff += Date::IsLeapYear(year1) ? Date::LEAP_DAYS[month1] : Date::NORMAL_DAYS[month1];
+		if (is_negative) {
+			day_diff += Date::IsLeapYear(ts1.year) ? Date::LEAP_DAYS[ts1.month] : Date::NORMAL_DAYS[ts1.month];
 			month_diff--;
 		} else {
-			day_diff += Date::IsLeapYear(year2) ? Date::LEAP_DAYS[month2] : Date::NORMAL_DAYS[month2];
+			day_diff += Date::IsLeapYear(ts2.year) ? Date::LEAP_DAYS[ts2.month] : Date::NORMAL_DAYS[ts2.month];
 			month_diff--;
 		}
 	}
@@ -432,6 +414,15 @@ interval_t Interval::GetAge(timestamp_t timestamp_1, timestamp_t timestamp_2) {
 	return interval;
 }
 
+interval_t Interval::GetAge(timestamp_t timestamp_1, timestamp_t timestamp_2) {
+	D_ASSERT(Timestamp::IsFinite(timestamp_1) && Timestamp::IsFinite(timestamp_2));
+
+	auto ts_component1 = Timestamp::GetComponents(timestamp_1);
+	auto ts_component2 = Timestamp::GetComponents(timestamp_2);
+
+	return Interval::GetAge(ts_component1, ts_component2, timestamp_1 < timestamp_2);
+}
+
 interval_t Interval::GetDifference(timestamp_t timestamp_1, timestamp_t timestamp_2) {
 	if (!Timestamp::IsFinite(timestamp_1) || !Timestamp::IsFinite(timestamp_2)) {
 		throw InvalidInputException("Cannot subtract infinite timestamps");
diff --git a/src/common/types/timestamp.cpp b/src/common/types/timestamp.cpp
index 838f2b559cb9..0cd4abc9d847 100644
--- a/src/common/types/timestamp.cpp
+++ b/src/common/types/timestamp.cpp
@@ -535,4 +535,16 @@ double Timestamp::GetJulianDay(timestamp_t timestamp) {
 	return result;
 }
 
+TimestampComponents Timestamp::GetComponents(timestamp_t timestamp) {
+	date_t date;
+	dtime_t time;
+
+	Convert(timestamp, date, time);
+
+	TimestampComponents result;
+	Date::Convert(date, result.year, result.month, result.day);
+	Time::Convert(time, result.hour, result.minute, result.second, result.microsecond);
+	return result;
+}
+
 } // namespace duckdb
diff --git a/src/include/duckdb/common/types/cast_helpers.hpp b/src/include/duckdb/common/types/cast_helpers.hpp
index f0087ba30b11..2d32e72fe642 100644
--- a/src/include/duckdb/common/types/cast_helpers.hpp
+++ b/src/include/duckdb/common/types/cast_helpers.hpp
@@ -192,55 +192,66 @@ struct UhugeintToStringCast {
 };
 
 struct DateToStringCast {
-	static idx_t Length(int32_t date[], idx_t &year_length, bool &add_bc) {
+	static idx_t YearLength(int32_t &year, idx_t &year_length, bool &add_bc) {
 		// format is YYYY-MM-DD with optional (BC) at the end
 		// regular length is 10
 		idx_t length = 6;
 		year_length = 4;
 		add_bc = false;
-		if (date[0] <= 0) {
+		if (year <= 0) {
 			// add (BC) suffix
 			length += 5;
-			date[0] = -date[0] + 1;
+			year = -year + 1;
 			add_bc = true;
 		}
 
 		// potentially add extra characters depending on length of year
-		year_length += date[0] >= 10000;
-		year_length += date[0] >= 100000;
-		year_length += date[0] >= 1000000;
-		year_length += date[0] >= 10000000;
+		year_length += year >= 10000;
+		year_length += year >= 100000;
+		year_length += year >= 1000000;
+		year_length += year >= 10000000;
 		length += year_length;
 		return length;
 	}
 
-	static void Format(char *data, int32_t date[], idx_t year_length, bool add_bc) {
+	static idx_t Length(int32_t date[], idx_t &year_length, bool &add_bc) {
+		return YearLength(date[0], year_length, add_bc);
+	}
+
+	static void FormatComponent(char *&ptr, int32_t number) {
+		ptr[0] = '-';
+		if (number < 10) {
+			ptr[1] = '0';
+			ptr[2] = UnsafeNumericCast<char>('0' + number);
+		} else {
+			auto index = UnsafeNumericCast<idx_t>(number * 2);
+			ptr[1] = duckdb_fmt::internal::data::digits[index];
+			ptr[2] = duckdb_fmt::internal::data::digits[index + 1];
+		}
+		ptr += 3;
+	}
+
+	static void Format(char *data, int32_t year, int32_t month, int32_t day, idx_t year_length, bool add_bc) {
 		// now we write the string, first write the year
 		auto endptr = data + year_length;
-		endptr = NumericHelper::FormatUnsigned(date[0], endptr);
+		endptr = NumericHelper::FormatUnsigned(year, endptr);
 		// add optional leading zeros
 		while (endptr > data) {
 			*--endptr = '0';
 		}
 		// now write the month and day
 		auto ptr = data + year_length;
-		for (int i = 1; i <= 2; i++) {
-			ptr[0] = '-';
-			if (date[i] < 10) {
-				ptr[1] = '0';
-				ptr[2] = UnsafeNumericCast<char>('0' + date[i]);
-			} else {
-				auto index = UnsafeNumericCast<idx_t>(date[i] * 2);
-				ptr[1] = duckdb_fmt::internal::data::digits[index];
-				ptr[2] = duckdb_fmt::internal::data::digits[index + 1];
-			}
-			ptr += 3;
-		}
+		FormatComponent(ptr, month);
+		FormatComponent(ptr, day);
 		// optionally add BC to the end of the date
 		if (add_bc) {
 			memcpy(ptr, " (BC)", 5); // NOLINT
 		}
 	}
+
+	static void Format(char *data, int32_t date[], idx_t year_length, bool add_bc) {
+		Format(data, date[0], date[1], date[2], year_length, add_bc);
+	}
 };
 
 struct TimeToStringCast {
@@ -261,11 +272,11 @@ struct TimeToStringCast {
 		return UnsafeNumericCast<int32_t>(trailing_zeros);
 	}
 
-	static idx_t Length(int32_t time[], char micro_buffer[]) {
+	static idx_t MicrosLength(int32_t micros, char micro_buffer[]) {
 		// format is HH:MM:DD.MS
 		// microseconds come after the time with a period separator
 		idx_t length;
-		if (time[3] == 0) {
+		if (micros == 0) {
 			// no microseconds
 			// format is HH:MM:DD
 			length = 8;
@@ -276,11 +287,15 @@ struct TimeToStringCast {
 			// we write backwards and pad with zeros to the left
 			// now we figure out how many digits we need to include by looking backwards
 			// and checking how many zeros we encounter
-			length -= NumericCast<idx_t>(FormatMicros(time[3], micro_buffer));
+			length -= NumericCast<idx_t>(FormatMicros(micros, micro_buffer));
 		}
 		return length;
 	}
 
+	static idx_t Length(int32_t time[], char micro_buffer[]) {
+		return MicrosLength(time[3], micro_buffer);
+	}
+
 	static void FormatTwoDigits(char *ptr, int32_t value) {
 		D_ASSERT(value >= 0 && value <= 99);
 		if (value < 10) {
@@ -293,21 +308,24 @@ struct TimeToStringCast {
 		}
 	}
 
-	static void Format(char *data, idx_t length, int32_t time[], char micro_buffer[]) {
+	static void Format(char *data, idx_t length, int32_t hour, int32_t minute, int32_t second, int32_t microsecond,
+	                   char micro_buffer[]) {
 		// first write hour, month and day
-		auto ptr = data;
-		ptr[2] = ':';
-		ptr[5] = ':';
-		for (int i = 0; i <= 2; i++) {
-			FormatTwoDigits(ptr, time[i]);
-			ptr += 3;
-		}
+		FormatTwoDigits(data, hour);
+		data[2] = ':';
+		FormatTwoDigits(data + 3, minute);
+		data[5] = ':';
+		FormatTwoDigits(data + 6, second);
 		if (length > 8) {
 			// write the micro seconds at the end
 			data[8] = '.';
 			memcpy(data + 9, micro_buffer, length - 9);
 		}
 	}
+
+	static void Format(char *data, idx_t length, int32_t time[], char micro_buffer[]) {
+		Format(data, length, time[0], time[1], time[2], time[3], micro_buffer);
+	}
 };
 
 struct IntervalToStringCast {
diff --git a/src/include/duckdb/common/types/interval.hpp b/src/include/duckdb/common/types/interval.hpp
index 825000d57151..23c9b2052637 100644
--- a/src/include/duckdb/common/types/interval.hpp
+++ b/src/include/duckdb/common/types/interval.hpp
@@ -17,6 +17,7 @@ struct dtime_t;     // NOLINT: literal casing
 struct date_t;      // NOLINT: literal casing
 struct dtime_tz_t;  // NOLINT: literal casing
 struct timestamp_t; // NOLINT: literal casing
+struct TimestampComponents;
 
 class Serializer;
 class Deserializer;
@@ -144,8 +145,10 @@ class Interval {
 	//! Get Interval in Nanoseconds
 	static int64_t GetNanoseconds(const interval_t &val);
 
-	//! Returns the age between two timestamps (including 30 day months)
+	//! Returns the age between two timestamps (including months)
 	static interval_t GetAge(timestamp_t timestamp_1, timestamp_t timestamp_2);
+	//! Returns the age between two timestamp components
+	static interval_t GetAge(TimestampComponents ts1, TimestampComponents ts2, bool is_negative);
 
 	//! Returns the exact difference between two timestamps (days and seconds)
 	static interval_t GetDifference(timestamp_t timestamp_1, timestamp_t timestamp_2);
diff --git a/src/include/duckdb/common/types/timestamp.hpp b/src/include/duckdb/common/types/timestamp.hpp
index e061356a68ef..57835db22b82 100644
--- a/src/include/duckdb/common/types/timestamp.hpp
+++ b/src/include/duckdb/common/types/timestamp.hpp
@@ -119,6 +119,17 @@ enum class TimestampCastResult : uint8_t {
 	STRICT_UTC
 };
 
+struct TimestampComponents {
+	int32_t year;
+	int32_t month;
+	int32_t day;
+
+	int32_t hour;
+	int32_t minute;
+	int32_t second;
+	int32_t microsecond;
+};
+
 //! The static Timestamp class holds helper functions for the timestamp types.
 class Timestamp {
 public:
@@ -205,6 +216,9 @@ class Timestamp {
 	//! Convert a timestamp to a Julian Day
 	DUCKDB_API static double GetJulianDay(timestamp_t timestamp);
 
+	//! Decompose a timestamp into its components
+	DUCKDB_API static TimestampComponents GetComponents(timestamp_t timestamp);
+
 	DUCKDB_API static bool TryParseUTCOffset(const char *str, idx_t &pos, idx_t len, int &hh, int &mm, int &ss);
 
 	DUCKDB_API static string FormatError(const string &str);
diff --git a/test/sql/function/timestamp/test_icu_age.test b/test/sql/function/timestamp/test_icu_age.test
index 7c3dc8c4e422..6c5ecc8772f7 100644
--- a/test/sql/function/timestamp/test_icu_age.test
+++ b/test/sql/function/timestamp/test_icu_age.test
@@ -64,21 +64,21 @@ INSERT INTO timestamps VALUES
 query T
 SELECT AGE(t1, TIMESTAMPTZ '1957-06-13') FROM timestamps;
 ----
-43 years 9 months 28 days
+43 years 9 months 27 days
 56 years 10 months 12 days
 56 years 10 months 12 days
-61 years 11 months 29 days
+61 years 11 months 28 days
 NULL
-61 years 11 months 29 days
+61 years 11 months 28 days
 NULL
 
 # Time fields caused by DST
 query T
 SELECT AGE(TIMESTAMPTZ '2001-04-10', t2) FROM timestamps;
 ----
-43 years 9 months 28 days
+43 years 9 months 27 days
 -13 years -7 days
--12 years -8 months -22 days
+-12 years -8 months -21 days
 -18 years -2 months -1 day
 -18 years -2 months -1 day
 NULL
@@ -87,7 +87,7 @@ NULL
 query T
 SELECT AGE(t1, t2) FROM timestamps;
 ----
-43 years 9 months 28 days
+43 years 9 months 27 days
 8 days
 3 months 24 days
 00:00:00
diff --git a/test/sql/timezone/icu_calendar_operations.test b/test/sql/timezone/icu_calendar_operations.test
new file mode 100644
index 000000000000..7070e52c9301
--- /dev/null
+++ b/test/sql/timezone/icu_calendar_operations.test
@@ -0,0 +1,27 @@
+# name: test/sql/timezone/icu_calendar_operations.test
+# description: Test the ICU calendar interface
+# group: [timezone]
+
+require icu
+
+# Normalise the testing locale
+statement ok
+SET Calendar = 'hebrew';
+
+statement ok
+SET TimeZone = 'UTC';
+
+query I
+SELECT AGE(timestamptz '2020-01-01' + interval (30) day, timestamptz '2020-01-01')
+----
+1 month
+
+query I
+SELECT AGE(timestamptz '2020-06-01' + interval (230) day, timestamptz '2020-06-01')
+----
+7 months 23 days
+
+query I
+SELECT AGE(timestamptz '2021-06-01', timestamptz '2020-06-01' + interval (230) day)
+----
+5 months 7 days


Reviews:
user: login: hawkfish
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T05:49:03Z
user: login: hawkfish
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T05:53:34Z
user: login: Mytherin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-30T06:28:01Z
user: login: hawkfish
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T08:05:41Z
user: login: hawkfish
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T08:06:45Z
user: login: hawkfish
body: Interval::GetAge has two references to MONTH_PER_YEAR so I think it doesn't work for 13 month calendars.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T08:23:02Z

Review comments:
diff_hunk: @@ -192,55 +192,66 @@ struct UhugeintToStringCast {
 };
 
 struct DateToStringCast {
-	static idx_t Length(int32_t date[], idx_t &year_length, bool &add_bc) {
+	static idx_t Length(int32_t &year, idx_t &year_length, bool &add_bc) {
path: src/include/duckdb/common/types/cast_helpers.hpp
user: login: hawkfish
body: Maybe we should call this `YearLength`?
created_at: 2025-03-30T05:39:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -261,11 +272,11 @@ struct TimeToStringCast {
 		return UnsafeNumericCast<int32_t>(trailing_zeros);
 	}
 
-	static idx_t Length(int32_t time[], char micro_buffer[]) {
+	static idx_t Length(int32_t micros, char micro_buffer[]) {
path: src/include/duckdb/common/types/cast_helpers.hpp
user: login: hawkfish
body: Maybe `MicrosLength`?
created_at: 2025-03-30T05:42:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+# name: test/sql/timezone/icu_calendar_operations.test
+# description: Test the ICU calendar interface
+# group: [timezone]
+
+require icu
+
+# Normalise the testing locale
+statement ok
+SET Calendar = 'hebrew';
path: test/sql/timezone/icu_calendar_operations.test
user: login: hawkfish
body: We should test a year in the Hebrew calendar that has Adar II (the 13th month.) [Mid-March 2024](https://www.hebcal.com/holidays/rosh-chodesh-adar-ii-2024) is an example.
created_at: 2025-03-30T05:48:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+# name: test/sql/timezone/icu_calendar_operations.test
+# description: Test the ICU calendar interface
+# group: [timezone]
+
+require icu
+
+# Normalise the testing locale
+statement ok
+SET Calendar = 'hebrew';
path: test/sql/timezone/icu_calendar_operations.test
user: login: hawkfish
body: (Also, calendar name are case-insensitive, so we should probably capitalise "Hebrew")
created_at: 2025-03-30T05:53:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+# name: test/sql/timezone/icu_calendar_operations.test
+# description: Test the ICU calendar interface
+# group: [timezone]
+
+require icu
+
+# Normalise the testing locale
+statement ok
+SET Calendar = 'hebrew';
path: test/sql/timezone/icu_calendar_operations.test
user: login: Mytherin
body: I used 2020 because it has the 13th month:

```sql
set calendar='hebrew';
select timestamptz '2020-06-01' + interval (230) day as res;
┌───────────────────────────┐
│            res            │
│ timestamp with time zone  │
├───────────────────────────┤
│ 2020-13-24 00:00:00+00:17 │
└───────────────────────────┘
```

Of course the years mean something different as well (it is currently the year 5785 in the hebrew calendar - 2020 is actually several hundred years B.C.)
created_at: 2025-03-30T06:28:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,22 @@
+# name: test/sql/timezone/icu_calendar_operations.test
+# description: Test the ICU calendar interface
+# group: [timezone]
+
+require icu
+
+# Normalise the testing locale
+statement ok
+SET Calendar = 'hebrew';
path: test/sql/timezone/icu_calendar_operations.test
user: login: hawkfish
body: Ah I forgot how that gets parsed - nicely done!
created_at: 2025-03-30T08:06:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: Mytherin
created_at: 2025-03-30T08:33:22Z
body: Yep you're right - my test "accidentally" worked with it, but I found a scenario where it doesn't work correctly.
author_association: COLLABORATOR
user: login: Mytherin
created_at: 2025-03-30T08:53:53Z
body: We also need to know the amount of days in a given month to be able to correctly compute the age. For now I'll fallback to the previous ICU implementation for non-Gregorian calendars.
author_association: COLLABORATOR