@@ -160,31 +160,59 @@ impl DateTimeBuilder {
160160 self . set_time ( time)
161161 }
162162
163+ /// Build a `Zoned` object from the pieces accumulated in this builder.
164+ ///
165+ /// Resolution order (mirrors GNU `date` semantics):
166+ ///
167+ /// 1. Base instant.
168+ /// a. If `self.base` is provided, start with it.
169+ /// b. Else if a `timezone` rule is present, start with "now" in that
170+ /// timezone.
171+ /// c. Else start with current system local time.
172+ ///
173+ /// 2. Absolute timestamp override.
174+ /// a. If `self.timestamp` is set, it fully determines the result.
175+ ///
176+ /// 3. Time of day truncation.
177+ /// a. If any of date, time, weekday, offset, timezone is set, zero the
178+ /// time of day to 00:00:00 before applying fields.
179+ ///
180+ /// 4. Fieldwise resolution (applied to the base instant).
181+ /// a. Apply date. If year is absent in the parsed date, inherit the year
182+ /// from the base instant.
183+ /// b. Apply time. If time carries an explicit numeric offset, apply the
184+ /// offset before setting time.
185+ /// c. Apply weekday (e.g., "next Friday" or "last Monday").
186+ /// d. Apply relative adjustments (e.g., "+3 days", "-2 months").
187+ /// e. Apply final fixed offset if present.
163188 pub ( super ) fn build ( self ) -> Result < Zoned , error:: Error > {
164- let base = self . base . unwrap_or ( if let Some ( tz) = & self . timezone {
165- jiff:: Timestamp :: now ( ) . to_zoned ( tz. clone ( ) )
166- } else {
167- Zoned :: now ( )
168- } ) ;
189+ // 1. Choose the base instant.
190+ let base = match ( self . base , & self . timezone ) {
191+ ( Some ( b) , _) => b,
192+ ( None , Some ( tz) ) => jiff:: Timestamp :: now ( ) . to_zoned ( tz. clone ( ) ) ,
193+ ( None , None ) => Zoned :: now ( ) ,
194+ } ;
169195
170- // If a timestamp is set, we use it to build the `Zoned` object .
196+ // 2. Absolute timestamp override everything else .
171197 if let Some ( ts) = self . timestamp {
172- return Ok ( jiff:: Timestamp :: try_from ( ts) ?. to_zoned ( base. offset ( ) . to_time_zone ( ) ) ) ;
198+ let ts = jiff:: Timestamp :: try_from ( ts) ?;
199+ return Ok ( ts. to_zoned ( base. offset ( ) . to_time_zone ( ) ) ) ;
173200 }
174201
175- // If any of the following items are set, we truncate the time portion
176- // of the base date to zero; otherwise, we use the base date as is.
177- let mut dt = if self . date . is_none ( )
178- && self . time . is_none ( )
179- && self . weekday . is_none ( )
180- && self . offset . is_none ( )
181- && self . timezone . is_none ( )
182- {
183- base
184- } else {
202+ // 3. Determine whether to truncate the time of day.
203+ let need_midnight = self . date . is_some ( )
204+ || self . time . is_some ( )
205+ || self . weekday . is_some ( )
206+ || self . offset . is_some ( )
207+ || self . timezone . is_some ( ) ;
208+
209+ let mut dt = if need_midnight {
185210 base. with ( ) . time ( civil:: time ( 0 , 0 , 0 , 0 ) ) . build ( ) ?
211+ } else {
212+ base
186213 } ;
187214
215+ // 4a. Apply date.
188216 if let Some ( date) = self . date {
189217 let d: civil:: Date = if date. year . is_some ( ) {
190218 date. try_into ( ) ?
@@ -194,6 +222,7 @@ impl DateTimeBuilder {
194222 dt = dt. with ( ) . date ( d) . build ( ) ?;
195223 }
196224
225+ // 4b. Apply time.
197226 if let Some ( time) = self . time . clone ( ) {
198227 if let Some ( offset) = & time. offset {
199228 dt = dt. datetime ( ) . to_zoned ( offset. try_into ( ) ?) ?;
@@ -203,21 +232,21 @@ impl DateTimeBuilder {
203232 dt = dt. with ( ) . time ( t) . build ( ) ?;
204233 }
205234
206- if let Some ( weekday:: Weekday { offset, day } ) = self . weekday {
235+ // 4c. Apply weekday.
236+ if let Some ( weekday:: Weekday { mut offset, day } ) = self . weekday {
207237 if self . time . is_none ( ) {
208238 dt = dt. with ( ) . time ( civil:: time ( 0 , 0 , 0 , 0 ) ) . build ( ) ?;
209239 }
210240
211- let mut offset = offset;
212- let day = day. into ( ) ;
241+ let target = day. into ( ) ;
213242
214243 // If the current day is not the target day, we need to adjust
215244 // the x value to ensure we find the correct day.
216245 //
217246 // Consider this:
218247 // Assuming today is Monday, next Friday is actually THIS Friday;
219248 // but next Monday is indeed NEXT Monday.
220- if dt. date ( ) . weekday ( ) != day && offset > 0 {
249+ if dt. date ( ) . weekday ( ) != target && offset > 0 {
221250 offset -= 1 ;
222251 }
223252
@@ -237,14 +266,15 @@ impl DateTimeBuilder {
237266 //
238267 // Example 4: next Thursday (x = 1, day = Thursday)
239268 // delta = (3 - 3) % 7 + (1) * 7 = 7
240- let delta = ( day . since ( civil:: Weekday :: Monday ) as i32
269+ let delta = ( target . since ( civil:: Weekday :: Monday ) as i32
241270 - dt. date ( ) . weekday ( ) . since ( civil:: Weekday :: Monday ) as i32 )
242271 . rem_euclid ( 7 )
243272 + offset. checked_mul ( 7 ) . ok_or ( "multiplication overflow" ) ?;
244273
245274 dt = dt. checked_add ( Span :: new ( ) . try_days ( delta) ?) ?;
246275 }
247276
277+ // 4d. Apply relative adjustments.
248278 for rel in self . relative {
249279 dt = dt. checked_add :: < Span > ( if let relative:: Relative :: Months ( x) = rel {
250280 // *NOTE* This is done in this way to conform to GNU behavior.
@@ -255,6 +285,7 @@ impl DateTimeBuilder {
255285 } ) ?;
256286 }
257287
288+ // 4e. Apply final fixed offset.
258289 if let Some ( offset) = self . offset {
259290 let ( offset, hour_adjustment) = offset. normalize ( ) ;
260291 dt = dt. checked_add ( Span :: new ( ) . hours ( hour_adjustment) ) ?;
0 commit comments