go-gorm / datatypes

GORM Customized Data Types Collection
https://gorm.io/docs/data_types.html
MIT License
698 stars 107 forks source link

Interval Type (ISO 8601) #267

Open meftunca opened 2 weeks ago

meftunca commented 2 weeks ago

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)),
    }
}