Skip to content

Interval Type (ISO 8601) #267

@meftunca

Description

@meftunca

Describe the feature
The feature request is to add support for interval types in GORM, a popular ORM library for Go. This includes the ability to define, query, and manipulate interval data types directly within GORM models. Interval types are commonly used in databases to represent periods of time, and supporting them natively in GORM would enhance its functionality for time-based data handling.

Motivation
The motivation behind this feature is to provide a more comprehensive and efficient way to handle time intervals within GORM. Many applications, especially those involving scheduling, event management, or time-series data, require the use of interval types. By integrating support for interval types directly into GORM, developers can streamline their code, reduce the need for custom solutions, and improve the overall performance and reliability of their applications.

Below I share an example that can be integrated

package types

import (
	"database/sql/driver"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

// TimeInterval Holds duration/interval information received from PostgreSQL,
// supports each format except verbose. JSON marshals to a clock format "HH:MM:SS".
// Uses a Valid property so it can be nullable
type TimeInterval struct {
	Valid   bool
	Years   uint16
	Months  uint8
	Days    uint8
	Hours   uint8
	Minutes uint8
	Seconds uint8
}

// Value Implements driver.Value
func (t TimeInterval) Value() (driver.Value, error) {
	return fmt.Sprintf("P%dY%dM%dDT%02dH%02dM%02dS", t.Years, t.Months, t.Days, t.Hours, t.Minutes, t.Seconds), nil
}

// Scan Implements sql.Scanner
func (t *TimeInterval) Scan(src interface{}) error {
	bytes, ok := src.([]byte)
	if !ok {
		if srcStr, ok := src.(string); ok {
			bytes = []byte(srcStr)
		} else {
			//Probably nil
			t.Valid = false
			t.Years = 0
			t.Months = 0
			t.Days = 0
			t.Hours = 0
			t.Minutes = 0
			t.Seconds = 0
			return nil
		}
	}
	str := strings.ToUpper(string(bytes))
	if len(str) == 0 {
		return errors.New("received bytes for TimeInterval but string ended up empty")
	}

	if str[0] != 'P' {
		return errors.New("invalid ISO 8601 format")
	}

	datePortion := str[1:strings.Index(str, "T")]
	timePortion := str[strings.Index(str, "T")+1:]

	// Parse date portion
	for _, part := range strings.Split(datePortion, "") {
		if len(part) > 0 {
			value, err := strconv.ParseUint(part[:len(part)-1], 10, 32)
			if err != nil {
				return err
			}
			switch part[len(part)-1] {
			case 'Y':
				t.Years = uint16(value)
			case 'M':
				t.Months = uint8(value)
			case 'D':
				t.Days = uint8(value)
			}
		}
	}

	// Parse time portion
	for _, part := range strings.Split(timePortion, "") {
		if len(part) > 0 {
			value, err := strconv.ParseUint(part[:len(part)-1], 10, 32)
			if err != nil {
				return err
			}
			switch part[len(part)-1] {
			case 'H':
				t.Hours = uint8(value)
			case 'M':
				t.Minutes = uint8(value)
			case 'S':
				t.Seconds = uint8(value)
			}
		}
	}

	t.Valid = true
	return nil
}

// MarshalJSON Marshals JSON
func (t TimeInterval) MarshalJSON() ([]byte, error) {
	if t.Valid {
		return []byte(fmt.Sprintf("\"P%dY%dM%dDT%02dH%02dM%02dS\"", t.Years, t.Months, t.Days, t.Hours, t.Minutes, t.Seconds)), nil
	}

	return []byte("null"), nil
}

// ToSeconds Returns the culminative seconds of this interval
func (t TimeInterval) ToSeconds() uint {
	return uint(t.Years*365*24*60*60) + uint(t.Months*30*24*60*60) + uint(t.Days*24*60*60) + uint(t.Hours*60*60) + uint(t.Minutes*60) + uint(t.Seconds)
}

// UnmarshalJSON Implements JSON marshalling
func (t *TimeInterval) UnmarshalJSON(data []byte) error {
	str := string(data)
	if str[0] != '"' {
		if str == "null" {
			t.Valid = false
			return nil
		}

		return fmt.Errorf("expected TimeInterval to be a string, received %s instead", str)
	}

	str = str[1 : len(str)-1]
	if len(str) == 0 {
		t.Years = 0
		t.Months = 0
		t.Days = 0
		t.Hours = 0
		t.Minutes = 0
		t.Seconds = 0
		t.Valid = false
		return nil
	}

	if str[0] != 'P' {
		return errors.New("invalid ISO 8601 format")
	}

	datePortion := str[1:strings.Index(str, "T")]
	timePortion := str[strings.Index(str, "T")+1:]

	// Parse date portion
	for _, part := range strings.Split(datePortion, "") {
		if len(part) > 0 {
			value, err := strconv.ParseUint(part[:len(part)-1], 10, 32)
			if err != nil {
				return err
			}
			switch part[len(part)-1] {
			case 'Y':
				t.Years = uint16(value)
			case 'M':
				t.Months = uint8(value)
			case 'D':
				t.Days = uint8(value)
			}
		}
	}

	// Parse time portion
	for _, part := range strings.Split(timePortion, "") {
		if len(part) > 0 {
			value, err := strconv.ParseUint(part[:len(part)-1], 10, 32)
			if err != nil {
				return err
			}
			switch part[len(part)-1] {
			case 'H':
				t.Hours = uint8(value)
			case 'M':
				t.Minutes = uint8(value)
			case 'S':
				t.Seconds = uint8(value)
			}
		}
	}

	t.Valid = true
	return nil
}

// NewTimeIntervalFromDuration Converts a duration, into a TimeInterval object
func NewTimeIntervalFromDuration(d time.Duration) TimeInterval {
	hrs := uint8(d.Hours() / 24)
	mins := uint8((d.Hours() - float64(hrs*24)) * 60)
	secs := uint8((d.Minutes() - float64(mins*60)) * 60)
	return TimeInterval{
		Valid:   true,
		Years:   0,
		Months:  0,
		Days:    hrs,
		Hours:   mins,
		Minutes: secs,
		Seconds: uint8(d.Seconds() - float64(secs*60)),
	}
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions