iso8601

package module
v0.0.0-...-b0314ba Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2025 License: MIT Imports: 8 Imported by: 0

README

ISO8601 Duration Go Report Card GoDoc

A Go package for parsing and manipulating ISO8601-formatted durations with support for fractional seconds and date/time shifting.

Why This Library?

This library provides a comprehensive, well-tested solution for ISO8601 duration handling in Go with several key advantages:

  • ✅ High Test Coverage - 88.8% code coverage with comprehensive test suite
  • ✅ ISO8601 Compliant - Follows the ISO8601 standard for duration representation
  • ✅ DST-Aware - Correctly handles daylight saving time transitions when shifting dates
  • ✅ Production Ready - Clean API, comprehensive documentation, and robust error handling
  • ✅ Feature Rich - Supports fractional seconds, negative durations, arithmetic operations, and more
  • ✅ Interoperable - Seamless conversion to/from Go's time.Duration
  • ✅ JSON Ready - Built-in JSON marshaling/unmarshaling support
  • ✅ Zero Dependencies - Uses only Go standard library

Features

  • Parse ISO8601 duration strings (e.g., P1Y2M3DT4H5M6.5S)
  • Support for fractional seconds (e.g., P343DT13H8M33.3444S)
  • Support for negative durations (e.g., -P1D, -PT1H)
  • Duration arithmetic (Add, Subtract, Multiply)
  • Comparison methods (Equal, LessThan, GreaterThan)
  • Conversion to/from Go's time.Duration
  • Shift dates/times forward and backward
  • JSON marshaling/unmarshaling
  • Handles DST transitions correctly

Basic Example

package main

import (
	"fmt"
	"time"

	"github.com/AbhijitDhariya/iso8601"
)

func main() {
	d, _ := iso8601.ParseISO8601("P1D")
	today := time.Now()
	tomorrow := d.Shift(today)
	yesterday := d.Unshift(today)
	fmt.Println(today.Format("Jan _2"))
	fmt.Println(tomorrow.Format("Jan _2"))
	fmt.Println(yesterday.Format("Jan _2"))
}

Fractional Seconds

The package supports fractional seconds in ISO8601 duration strings:

d, _ := iso8601.ParseISO8601("P343DT13H8M33.3444S")
fmt.Println(d.String()) // Output: P343DT13H8M33.3444S

d2, _ := iso8601.ParseISO8601("PT0.5S")
fmt.Println(d2.String()) // Output: PT0.5S

Negative Durations

The package supports negative durations with a leading minus sign:

d, _ := iso8601.ParseISO8601("-P1D")
fmt.Println(d.String()) // Output: -P1D

d2, _ := iso8601.ParseISO8601("-PT1H30M")
fmt.Println(d2.String()) // Output: -PT1H30M

// Check if a duration is negative
if d.IsNegative() {
    fmt.Println("Duration is negative")
}

// Negate a duration
positive := d.Negate()
fmt.Println(positive.String()) // Output: P1D
Use Cases for Negative Durations

Negative durations are particularly useful in several scenarios:

1. Time Differences and Deltas

When calculating the difference between two times, negative durations naturally represent when the end time is before the start time:

start := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
end := time.Date(2024, 1, 14, 10, 0, 0, 0, time.UTC) // One day earlier

// Calculate duration between times
diff := end.Sub(start) // This will be negative
d := iso8601.FromTimeDuration(diff)
fmt.Println(d.String()) // Output: -PT24H
2. Deadline Tracking

Represent time until or past a deadline:

deadline := time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC)
now := time.Now()

// Time until deadline (could be negative if past due)
remaining := deadline.Sub(now)
d := iso8601.FromTimeDuration(remaining)

if d.IsNegative() {
    fmt.Printf("Deadline passed %s ago\n", d.Negate().String())
} else {
    fmt.Printf("Time remaining: %s\n", d.String())
}
3. Offset Calculations

Represent timezone offsets or scheduling offsets that go backward:

// Represent a timezone offset (e.g., UTC-5)
offset, _ := iso8601.ParseISO8601("-PT5H")
baseTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
adjustedTime := offset.Shift(baseTime) // Goes backward 5 hours
4. API Responses and Data Models

Many APIs return durations that can be negative (e.g., "time until event" or "time since event"):

// Example: API returns "-PT2H" meaning "2 hours ago"
apiResponse := "-PT2H"
d, _ := iso8601.ParseISO8601(apiResponse)

if d.IsNegative() {
    eventTime := d.Shift(time.Now()) // Calculate when event occurred
    fmt.Printf("Event occurred at: %s\n", eventTime.Format(time.RFC3339))
}
5. Arithmetic with Time Differences

When subtracting durations, negative results are natural:

d1, _ := iso8601.ParseISO8601("PT1H")
d2, _ := iso8601.ParseISO8601("PT2H")

result := d1.Subtract(d2) // PT1H - PT2H = -PT1H
fmt.Println(result.String()) // Output: -PT1H
6. Scientific and Measurement Contexts

In contexts where negative values represent going backward or reversing direction:

// Example: Representing a time shift that goes backward
backwardShift, _ := iso8601.ParseISO8601("-P1DT6H")
originalTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
shiftedTime := backwardShift.Shift(originalTime)
// Result: 1 day and 6 hours earlier

Duration Arithmetic

Perform arithmetic operations on durations:

d1, _ := iso8601.ParseISO8601("P1DT2H")
d2, _ := iso8601.ParseISO8601("P2DT3H")

// Add durations
sum := d1.Add(d2)
fmt.Println(sum.String()) // Output: P3DT5H

// Subtract durations
diff := d2.Subtract(d1)
fmt.Println(diff.String()) // Output: P1DT1H

// Multiply a duration
doubled := d1.Multiply(2)
fmt.Println(doubled.String()) // Output: P2DT4H

Note: Arithmetic operations perform component-wise addition/subtraction. For durations with months/years, the result may not represent the exact calendar duration due to variable month lengths.

Comparison Methods

Compare durations using the comparison methods:

d1, _ := iso8601.ParseISO8601("PT1H30M")
d2, _ := iso8601.ParseISO8601("PT2H")

// Check equality
if d1.Equal(d2) {
    fmt.Println("Durations are equal")
}

// Compare durations
if d1.LessThan(d2) {
    fmt.Println("d1 is less than d2")
}

if d2.GreaterThan(d1) {
    fmt.Println("d2 is greater than d1")
}

Note: Comparison is most meaningful for time-only durations (no years/months/weeks/days). For durations with date components, the result may be ambiguous due to variable month lengths.

Conversion to/from time.Duration

Convert between ISO8601 durations and Go's time.Duration:

// Convert ISO8601 duration to time.Duration (time component only)
d, _ := iso8601.ParseISO8601("PT1H30M45.5S")
td := d.ToTimeDuration()
fmt.Println(td) // Output: 1h30m45.5s

// Create ISO8601 duration from time.Duration
td2 := 2*time.Hour + 30*time.Minute
d2 := iso8601.FromTimeDuration(td2)
fmt.Println(d2.String()) // Output: PT2H30M

// Works with fractional seconds
td3 := 1500 * time.Millisecond
d3 := iso8601.FromTimeDuration(td3)
fmt.Println(d3.String()) // Output: PT1.5S

Note: ToTimeDuration() only converts the time component (hours, minutes, seconds). Date components (years, months, weeks, days) are ignored. FromTimeDuration() only sets the time component; date components are zero.

Why Does This Package Exist?

Why can't we just use a time.Duration and time.Add?

A very reasonable question.

The code below repeatedly adds 24 hours to a time.Time. You might expect the time on that date to stay the same, but there are not always 24 hours in a day. When the clocks change in New York, the time will skew by an hour. As you can see from the output, iso8601.Duration.Shift() can increment the date without shifting the time.

package main

import (
	"fmt"
	"time"

	"github.com/AbhijitDhariya/iso8601"
)

func main() {
	loc, _ := time.LoadLocation("America/New_York")
	d, _ := iso8601.ParseISO8601("P1D")
	t1, _ := time.ParseInLocation("Jan 2, 2006 at 3:04pm", "Jan 1, 2006 at 3:04pm", loc)
	t2 := t1
	for i := 0; i < 365; i++ {
		t1 = t1.Add(24 * time.Hour)
		t2 = d.Shift(t2)
		fmt.Printf("time.Add:%d    Duration.Shift:%d\n", t1.Hour(), t2.Hour())
	}
}

// Outputs
// time.Add:15    Duration.Shift:15
// time.Add:15    Duration.Shift:15
// time.Add:15    Duration.Shift:15
// ...
// time.Add:16    Duration.Shift:15
// time.Add:16    Duration.Shift:15
// time.Add:16    Duration.Shift:15
// ...

Months and Date Rolling

Months are tricky. Shifting by months uses time.AddDate(), which is great. However, be aware of how differing days in the month are accommodated. Dates will 'roll over' if the month you're shifting to has fewer days. e.g. if you start on Jan 30th and repeat every "P1M", you'll get this:

Jan 30, 2006
Mar 2, 2006
Apr 2, 2006
May 2, 2006
Jun 2, 2006
Jul 2, 2006
Aug 2, 2006
Sep 2, 2006
Oct 2, 2006
Nov 2, 2006
Dec 2, 2006
Jan 2, 2007

API

ParseISO8601

Parses an ISO8601 duration string:

d, err := iso8601.ParseISO8601("P1Y2M3DT4H5M6.5S")
Shift

Returns a time.Time, shifted forward by the duration from the given start:

d, _ := iso8601.ParseISO8601("P1D")
tomorrow := d.Shift(time.Now())

Note: Shift uses time.AddDate for years, months, weeks, and days, and so shares its limitations. In particular, shifting by months is not recommended unless the start date is before the 28th of the month. Otherwise, dates will roll over, e.g. Aug 31 + P1M = Oct 1.

Week and Day values will be combined as W*7 + D.

Unshift

Returns a time.Time, shifted back by the duration from the given start:

d, _ := iso8601.ParseISO8601("P1D")
yesterday := d.Unshift(time.Now())

Note: Unshift uses time.AddDate for years, months, weeks, and days, and so shares its limitations. In particular, shifting back by months is not recommended unless the start date is before the 28th of the month. Otherwise, dates will roll over, e.g. Oct 1 - P1M = Aug 31.

Week and Day values will be combined as W*7 + D.

String

Returns an ISO8601 representation of the duration:

d, _ := iso8601.ParseISO8601("P1Y2M3DT4H5M6.5S")
fmt.Println(d.String()) // Output: P1Y2M3DT4H5M6.5S
Arithmetic Operations

Perform arithmetic on durations:

d1, _ := iso8601.ParseISO8601("P1DT2H")
d2, _ := iso8601.ParseISO8601("P2DT3H")

sum := d1.Add(d2)           // Add durations
diff := d2.Subtract(d1)      // Subtract durations
doubled := d1.Multiply(2)    // Multiply by scalar
Comparison Methods

Compare durations:

d1, _ := iso8601.ParseISO8601("PT1H")
d2, _ := iso8601.ParseISO8601("PT2H")

if d1.Equal(d2) { ... }        // Check equality
if d1.LessThan(d2) { ... }    // Less than
if d2.GreaterThan(d1) { ... }  // Greater than
Conversion Methods

Convert to/from Go's time.Duration:

d, _ := iso8601.ParseISO8601("PT1H30M")
td := d.ToTimeDuration()                    // Convert to time.Duration
d2 := iso8601.FromTimeDuration(time.Hour)  // Create from time.Duration
Helper Methods
d, _ := iso8601.ParseISO8601("-P1D")

if d.IsNegative() { ... }  // Check if negative
negated := d.Negate()      // Negate duration
JSON Support

The Duration type implements json.Marshaler and json.Unmarshaler:

d, _ := iso8601.ParseISO8601("P1D")
data, _ := json.Marshal(d)
// data is []byte(`"P1D"`)

var d2 iso8601.Duration
json.Unmarshal(data, &d2)

Key Strengths

1. Robust Date/Time Handling

Unlike simple time.Duration arithmetic, this library correctly handles:

  • Daylight Saving Time (DST) transitions - Shifting by days maintains the time of day even when clocks change
  • Variable month lengths - Properly handles months with different numbers of days
  • Leap years - Correctly accounts for leap years when shifting dates
2. Comprehensive Feature Set
  • Fractional Seconds - Full support for sub-second precision (e.g., PT33.3444S)
  • Negative Durations - Handle backward time shifts and time differences naturally
  • Arithmetic Operations - Add, subtract, and multiply durations
  • Comparison Methods - Compare durations for equality and ordering
  • Conversion Utilities - Easy conversion to/from Go's time.Duration
3. Clean and Intuitive API

The API is designed to be simple and intuitive:

// Parse and use - straightforward
d, _ := iso8601.ParseISO8601("P1D")
tomorrow := d.Shift(time.Now())

// Arithmetic - natural and readable
sum := d1.Add(d2)
diff := d2.Subtract(d1)

// Conversion - seamless
td := d.ToTimeDuration()
d2 := iso8601.FromTimeDuration(time.Hour)
4. Well-Tested and Documented
  • 88.8% test coverage with comprehensive test cases
  • Extensive documentation with practical examples
  • Edge case handling for DST, negative values, fractional seconds
  • Clear documentation of limitations and best practices
5. Production Ready
  • Zero external dependencies - Uses only Go standard library
  • JSON support - Built-in marshaling/unmarshaling
  • Error handling - Proper error returns for invalid input
  • Performance - Efficient regex-based parsing
6. Standards Compliant
  • Follows ISO8601 duration format specification
  • Handles all standard components (years, months, weeks, days, hours, minutes, seconds)
  • Supports optional time separator (T)
  • Proper formatting with trailing zero removal

Installation

go get github.com/AbhijitDhariya/iso8601

License

See LICENSE file.

Documentation

Overview

Package iso8601 handles ISO8601-formatted durations.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Duration

type Duration struct {
	Y int
	M int
	W int
	D int
	// Time Component
	TH int
	TM int
	TS float64 // Seconds, can include fractional part (e.g., 33.3444)
}

Duration represents an ISO8601 Duration https://en.wikipedia.org/wiki/ISO_8601#Durations

func FromTimeDuration

func FromTimeDuration(td time.Duration) Duration

FromTimeDuration creates a Duration from a time.Duration. Only the time component is set; date components are zero.

func ParseISO8601

func ParseISO8601(from string) (Duration, error)

ParseISO8601 parses an ISO8601 duration string. Supports negative durations with a leading minus sign (e.g., -P1D).

func (Duration) Add

func (d Duration) Add(other Duration) Duration

Add returns a new Duration that is the sum of d and other. Note: This performs component-wise addition. For durations with months/years, the result may not represent the exact calendar duration due to variable month lengths.

func (Duration) Equal

func (d Duration) Equal(other Duration) bool

Equal returns true if d and other have identical components. Note: For durations with months/years, equal components may represent different calendar durations due to variable month lengths.

func (Duration) GreaterThan

func (d Duration) GreaterThan(other Duration) bool

GreaterThan returns true if d is greater than other. This comparison is only meaningful for time-only durations (no years/months/weeks/days). For durations with date components, the result may be ambiguous due to variable month lengths.

func (Duration) HasTimePart

func (d Duration) HasTimePart() bool

HasTimePart returns true if the time part of the duration is non-zero.

func (Duration) IsNegative

func (d Duration) IsNegative() bool

IsNegative returns true if the duration is negative.

func (Duration) IsZero

func (d Duration) IsZero() bool

IsZero reports whether d represents the zero duration, P0D.

func (Duration) LessThan

func (d Duration) LessThan(other Duration) bool

LessThan returns true if d is less than other. This comparison is only meaningful for time-only durations (no years/months/weeks/days). For durations with date components, the result may be ambiguous due to variable month lengths.

func (Duration) MarshalJSON

func (d Duration) MarshalJSON() ([]byte, error)

MarshalJSON satisfies json.Marshaler.

func (Duration) Multiply

func (d Duration) Multiply(n int) Duration

Multiply returns a new Duration with all components multiplied by n.

func (Duration) Negate

func (d Duration) Negate() Duration

Negate returns a new Duration with all components negated.

func (Duration) Shift

func (d Duration) Shift(t time.Time) time.Time

Shift returns a time.Time, shifted by the duration from the given start.

NB: Shift uses time.AddDate for years, months, weeks, and days, and so shares its limitations. In particular, shifting by months is not recommended unless the start date is before the 28th of the month. Otherwise, dates will roll over, e.g. Aug 31 + P1M = Oct 1.

Week and Day values will be combined as W*7 + D.

func (Duration) String

func (d Duration) String() string

String returns an ISO8601-ish representation of the duration.

func (Duration) Subtract

func (d Duration) Subtract(other Duration) Duration

Subtract returns a new Duration that is the difference of d and other. Note: This performs component-wise subtraction. For durations with months/years, the result may not represent the exact calendar duration due to variable month lengths.

func (Duration) ToTimeDuration

func (d Duration) ToTimeDuration() time.Duration

ToTimeDuration converts the time component of d to a time.Duration. Date components (years, months, weeks, days) are ignored.

func (*Duration) UnmarshalJSON

func (d *Duration) UnmarshalJSON(b []byte) error

UnmarshalJSON satisfies json.Unmarshaler.

func (Duration) Unshift

func (d Duration) Unshift(t time.Time) time.Time

Unshift returns a time.Time, shifted back by the duration from the given start.

NB: UnShift uses time.AddDate for years, months, weeks, and days, and so shares its limitations. In particular, shifting back by months is not recommended unless the start date is before the 28th of the month. Otherwise, dates will roll over, e.g. Oct 1 - P1M = Aug 31.

Week and Day values will be combined as W*7 + D.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL