package pgtype
import (
"database/sql/driver"
"encoding/binary"
"encoding/json"
"fmt"
"regexp"
"strconv"
"time"
"github.com/jackc/pgx/v5/internal/pgio"
)
type DateScanner interface {
ScanDate (v Date ) error
}
type DateValuer interface {
DateValue () (Date , error )
}
type Date struct {
Time time .Time
InfinityModifier InfinityModifier
Valid bool
}
func (d *Date ) ScanDate (v Date ) error {
*d = v
return nil
}
func (d Date ) DateValue () (Date , error ) {
return d , nil
}
const (
negativeInfinityDayOffset = -2147483648
infinityDayOffset = 2147483647
)
func (dst *Date ) Scan (src any ) error {
if src == nil {
*dst = Date {}
return nil
}
switch src := src .(type ) {
case string :
return scanPlanTextAnyToDateScanner {}.Scan ([]byte (src ), dst )
case time .Time :
*dst = Date {Time : src , Valid : true }
return nil
}
return fmt .Errorf ("cannot scan %T" , src )
}
func (src Date ) Value () (driver .Value , error ) {
if !src .Valid {
return nil , nil
}
if src .InfinityModifier != Finite {
return src .InfinityModifier .String (), nil
}
return src .Time , nil
}
func (src Date ) MarshalJSON () ([]byte , error ) {
if !src .Valid {
return []byte ("null" ), nil
}
var s string
switch src .InfinityModifier {
case Finite :
s = src .Time .Format ("2006-01-02" )
case Infinity :
s = "infinity"
case NegativeInfinity :
s = "-infinity"
}
return json .Marshal (s )
}
func (dst *Date ) UnmarshalJSON (b []byte ) error {
var s *string
err := json .Unmarshal (b , &s )
if err != nil {
return err
}
if s == nil {
*dst = Date {}
return nil
}
switch *s {
case "infinity" :
*dst = Date {Valid : true , InfinityModifier : Infinity }
case "-infinity" :
*dst = Date {Valid : true , InfinityModifier : -Infinity }
default :
t , err := time .ParseInLocation ("2006-01-02" , *s , time .UTC )
if err != nil {
return err
}
*dst = Date {Time : t , Valid : true }
}
return nil
}
type DateCodec struct {}
func (DateCodec ) FormatSupported (format int16 ) bool {
return format == TextFormatCode || format == BinaryFormatCode
}
func (DateCodec ) PreferredFormat () int16 {
return BinaryFormatCode
}
func (DateCodec ) PlanEncode (m *Map , oid uint32 , format int16 , value any ) EncodePlan {
if _ , ok := value .(DateValuer ); !ok {
return nil
}
switch format {
case BinaryFormatCode :
return encodePlanDateCodecBinary {}
case TextFormatCode :
return encodePlanDateCodecText {}
}
return nil
}
type encodePlanDateCodecBinary struct {}
func (encodePlanDateCodecBinary ) Encode (value any , buf []byte ) (newBuf []byte , err error ) {
date , err := value .(DateValuer ).DateValue ()
if err != nil {
return nil , err
}
if !date .Valid {
return nil , nil
}
var daysSinceDateEpoch int32
switch date .InfinityModifier {
case Finite :
tUnix := time .Date (date .Time .Year (), date .Time .Month (), date .Time .Day (), 0 , 0 , 0 , 0 , time .UTC ).Unix ()
dateEpoch := time .Date (2000 , 1 , 1 , 0 , 0 , 0 , 0 , time .UTC ).Unix ()
secSinceDateEpoch := tUnix - dateEpoch
daysSinceDateEpoch = int32 (secSinceDateEpoch / 86400 )
case Infinity :
daysSinceDateEpoch = infinityDayOffset
case NegativeInfinity :
daysSinceDateEpoch = negativeInfinityDayOffset
}
return pgio .AppendInt32 (buf , daysSinceDateEpoch ), nil
}
type encodePlanDateCodecText struct {}
func (encodePlanDateCodecText ) Encode (value any , buf []byte ) (newBuf []byte , err error ) {
date , err := value .(DateValuer ).DateValue ()
if err != nil {
return nil , err
}
if !date .Valid {
return nil , nil
}
switch date .InfinityModifier {
case Finite :
bc := false
year := date .Time .Year ()
if year <= 0 {
year = -year + 1
bc = true
}
yearBytes := strconv .AppendInt (make ([]byte , 0 , 6 ), int64 (year ), 10 )
for i := len (yearBytes ); i < 4 ; i ++ {
buf = append (buf , '0' )
}
buf = append (buf , yearBytes ...)
buf = append (buf , '-' )
if date .Time .Month () < 10 {
buf = append (buf , '0' )
}
buf = strconv .AppendInt (buf , int64 (date .Time .Month ()), 10 )
buf = append (buf , '-' )
if date .Time .Day () < 10 {
buf = append (buf , '0' )
}
buf = strconv .AppendInt (buf , int64 (date .Time .Day ()), 10 )
if bc {
buf = append (buf , " BC" ...)
}
case Infinity :
buf = append (buf , "infinity" ...)
case NegativeInfinity :
buf = append (buf , "-infinity" ...)
}
return buf , nil
}
func (DateCodec ) PlanScan (m *Map , oid uint32 , format int16 , target any ) ScanPlan {
switch format {
case BinaryFormatCode :
switch target .(type ) {
case DateScanner :
return scanPlanBinaryDateToDateScanner {}
}
case TextFormatCode :
switch target .(type ) {
case DateScanner :
return scanPlanTextAnyToDateScanner {}
}
}
return nil
}
type scanPlanBinaryDateToDateScanner struct {}
func (scanPlanBinaryDateToDateScanner ) Scan (src []byte , dst any ) error {
scanner := (dst ).(DateScanner )
if src == nil {
return scanner .ScanDate (Date {})
}
if len (src ) != 4 {
return fmt .Errorf ("invalid length for date: %v" , len (src ))
}
dayOffset := int32 (binary .BigEndian .Uint32 (src ))
switch dayOffset {
case infinityDayOffset :
return scanner .ScanDate (Date {InfinityModifier : Infinity , Valid : true })
case negativeInfinityDayOffset :
return scanner .ScanDate (Date {InfinityModifier : -Infinity , Valid : true })
default :
t := time .Date (2000 , 1 , int (1 +dayOffset ), 0 , 0 , 0 , 0 , time .UTC )
return scanner .ScanDate (Date {Time : t , Valid : true })
}
}
type scanPlanTextAnyToDateScanner struct {}
var dateRegexp = regexp .MustCompile (`^(\d{4,})-(\d\d)-(\d\d)( BC)?$` )
func (scanPlanTextAnyToDateScanner ) Scan (src []byte , dst any ) error {
scanner := (dst ).(DateScanner )
if src == nil {
return scanner .ScanDate (Date {})
}
sbuf := string (src )
match := dateRegexp .FindStringSubmatch (sbuf )
if match != nil {
year , err := strconv .ParseInt (match [1 ], 10 , 32 )
if err != nil {
return fmt .Errorf ("BUG: cannot parse date that regexp matched (year): %v" , err )
}
month , err := strconv .ParseInt (match [2 ], 10 , 32 )
if err != nil {
return fmt .Errorf ("BUG: cannot parse date that regexp matched (month): %v" , err )
}
day , err := strconv .ParseInt (match [3 ], 10 , 32 )
if err != nil {
return fmt .Errorf ("BUG: cannot parse date that regexp matched (month): %v" , err )
}
if len (match [4 ]) > 0 {
year = -year + 1
}
t := time .Date (int (year ), time .Month (month ), int (day ), 0 , 0 , 0 , 0 , time .UTC )
return scanner .ScanDate (Date {Time : t , Valid : true })
}
switch sbuf {
case "infinity" :
return scanner .ScanDate (Date {InfinityModifier : Infinity , Valid : true })
case "-infinity" :
return scanner .ScanDate (Date {InfinityModifier : -Infinity , Valid : true })
default :
return fmt .Errorf ("invalid date format" )
}
}
func (c DateCodec ) DecodeDatabaseSQLValue (m *Map , oid uint32 , format int16 , src []byte ) (driver .Value , error ) {
if src == nil {
return nil , nil
}
var date Date
err := codecScan (c , m , oid , format , src , &date )
if err != nil {
return nil , err
}
if date .InfinityModifier != Finite {
return date .InfinityModifier .String (), nil
}
return date .Time , nil
}
func (c DateCodec ) DecodeValue (m *Map , oid uint32 , format int16 , src []byte ) (any , error ) {
if src == nil {
return nil , nil
}
var date Date
err := codecScan (c , m , oid , format , src , &date )
if err != nil {
return nil , err
}
if date .InfinityModifier != Finite {
return date .InfinityModifier , nil
}
return date .Time , nil
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .