github.com/go-chrono/chrono@v0.0.0-20240102183611-532f0d0d7c34/duration.go (about)

     1  package chrono
     2  
     3  import (
     4  	"fmt"
     5  	"math/big"
     6  	"strconv"
     7  )
     8  
     9  // Duration represents a period of time with nanosecond precision,
    10  // with a range of approximately ±292,300,000,000 years.
    11  type Duration struct {
    12  	v big.Int
    13  }
    14  
    15  // DurationOf creates a new duration from the supplied extent.
    16  // Durations and extents are semantically equivalent, except that durations exceed,
    17  // and can therefore not be converted to, Go's basic types. Extents are represented as a single integer.
    18  func DurationOf(v Extent) Duration {
    19  	return durationOf(int64(v))
    20  }
    21  
    22  func durationOf(v int64) Duration {
    23  	return Duration{v: *big.NewInt(v)}
    24  }
    25  
    26  // Compare compares d with d2. If d is less than d2, it returns -1;
    27  // if d is greater than d2, it returns 1; if they're equal, it returns 0.
    28  func (d Duration) Compare(d2 Duration) int {
    29  	return d.v.Cmp(&d2.v)
    30  }
    31  
    32  // Add returns the duration d+d2.
    33  // If the operation would overflow the maximum duration, or underflow the minimum duration, it panics.
    34  // Use CanAdd to test whether aa panic would occur.
    35  func (d Duration) Add(d2 Duration) Duration {
    36  	out, err := d.add(d2)
    37  	if err != nil {
    38  		panic(err.Error())
    39  	}
    40  	return out
    41  }
    42  
    43  // CanAdd returns false if Add would panic if passed the same argument.
    44  func (d Duration) CanAdd(d2 Duration) bool {
    45  	_, err := d.add(d2)
    46  	return err == nil
    47  }
    48  
    49  func (d Duration) add(d2 Duration) (Duration, error) {
    50  	out := new(big.Int).Set(&d.v)
    51  	out.Add(out, &d2.v)
    52  
    53  	if out.Cmp(bigIntMinInt64) == -1 || out.Cmp(bigIntMaxInt64) == 1 {
    54  		return Duration{}, fmt.Errorf("duration out of range")
    55  	}
    56  	return Duration{v: *out}, nil
    57  }
    58  
    59  func (d Duration) mul(v int64) (Duration, error) {
    60  	out := new(big.Int).Set(&d.v)
    61  	out.Mul(out, big.NewInt(v))
    62  
    63  	if out.Cmp(bigIntMinInt64) == -1 || out.Cmp(bigIntMaxInt64) == 1 {
    64  		return Duration{}, fmt.Errorf("duration out of range")
    65  	}
    66  	return Duration{v: *out}, nil
    67  }
    68  
    69  // Nanoseconds returns the duration as a floating point number of nanoseconds.
    70  func (d Duration) Nanoseconds() float64 {
    71  	out, _ := new(big.Float).SetInt(&d.v).Float64()
    72  	return out
    73  }
    74  
    75  // Microseconds returns the duration as a floating point number of microseconds.
    76  func (d Duration) Microseconds() float64 {
    77  	out, _ := new(big.Float).Quo(new(big.Float).SetInt(&d.v), bigFloatMicrosecondExtent).Float64()
    78  	return out
    79  }
    80  
    81  // Milliseconds returns the duration as a floating point number of milliseconds.
    82  func (d Duration) Milliseconds() float64 {
    83  	out, _ := new(big.Float).Quo(new(big.Float).SetInt(&d.v), bigFloatMillisecondExtent).Float64()
    84  	return out
    85  }
    86  
    87  // Seconds returns the duration as a floating point number of seconds.
    88  func (d Duration) Seconds() float64 {
    89  	out, _ := new(big.Float).Quo(new(big.Float).SetInt(&d.v), bigFloatSecondExtent).Float64()
    90  	return out
    91  }
    92  
    93  // Minutes returns the duration as a floating point number of minutes.
    94  func (d Duration) Minutes() float64 {
    95  	out, _ := new(big.Float).Quo(new(big.Float).SetInt(&d.v), bigFloatMinuteExtent).Float64()
    96  	return out
    97  }
    98  
    99  // Hours returns the duration as a floating point number of hours.
   100  func (d Duration) Hours() float64 {
   101  	out, _ := new(big.Float).Quo(new(big.Float).SetInt(&d.v), bigFloatHourExtent).Float64()
   102  	return out
   103  }
   104  
   105  // String returns a string formatted according to ISO 8601.
   106  // It is equivalent to calling [Format] with no arguments.
   107  func (d Duration) String() string {
   108  	return d.Format()
   109  }
   110  
   111  // Units returns the whole numbers of hours, minutes, seconds, and nanosecond offset represented by d.
   112  func (d Duration) Units() (hours, mins, secs, nsec int) {
   113  	_secs, _nsec, _ := d.integers()
   114  	hours = int(_secs / 3600)
   115  	mins = int((_secs / 60) % 60)
   116  	secs = int(_secs % 60)
   117  	nsec = int(_nsec)
   118  	return
   119  }
   120  
   121  // Designator of date and time elements present in ISO 8601.
   122  type Designator rune
   123  
   124  // Designators.
   125  const (
   126  	Hours   Designator = 'H'
   127  	Minutes Designator = 'M'
   128  	Seconds Designator = 'S'
   129  )
   130  
   131  // Format the duration according to ISO 8601.
   132  // The output consists of only the time component - the period component is never included.
   133  // Thus the output always consists of "PT" followed by at least one unit of the time component (hours, minutes, seconds).
   134  //
   135  // The default format, obtained by calling the function with no arguments, consists of the most significant non-zero units,
   136  // presented non-breaking but trimmed.
   137  // 5 minutes is formatted as PT5M, trimming 0-value hours and seconds.
   138  // 1 hour and 5 seconds is formatted at PT1H0M5S, ensuring the sequence of units is not broken.
   139  //
   140  // A list of designators can be optionally passed to the function in order to control which units are included.
   141  // When passed, only those specified units are included in the formatted string, and are present regardless of whether their value is 0.
   142  //
   143  // Fractional values are automatically applied to the least significant unit, if applicable.
   144  // In order to format only integers, the round functions should be used before calling this function.
   145  func (d Duration) Format(exclusive ...Designator) string {
   146  	out, neg := d.format(exclusive...)
   147  	out = "P" + out
   148  	if neg {
   149  		out = "-" + out
   150  	}
   151  	return out
   152  }
   153  
   154  func (d Duration) format(exclusive ...Designator) (_ string, neg bool) {
   155  	secs, nsec, neg := d.integers()
   156  	return formatDuration(secs, nsec, neg, exclusive...)
   157  }
   158  
   159  func formatDuration(secs int64, nsec uint32, neg bool, exclusive ...Designator) (_ string, isNeg bool) {
   160  	values := make(map[Designator]float64, 3)
   161  	if len(exclusive) >= 1 {
   162  		for _, d := range exclusive {
   163  			values[d] = 0
   164  		}
   165  	}
   166  
   167  	_, h := values[Hours]
   168  	_, m := values[Minutes]
   169  	_, s := values[Seconds]
   170  
   171  	switch {
   172  	case len(exclusive) == 0:
   173  		if v := float64(secs / 3600); v != 0 {
   174  			values[Hours] = v
   175  			h = true
   176  		}
   177  	case h && (m || s):
   178  		values[Hours] = float64(secs / 3600)
   179  	case h:
   180  		values[Hours] = (float64(secs) / 3600) + (float64(nsec) / 3.6e12)
   181  	}
   182  
   183  	switch {
   184  	case len(exclusive) == 0:
   185  		if v := float64((secs % 3600) / 60); v != 0 {
   186  			values[Minutes] = v
   187  			m = true
   188  		}
   189  	case m && s && h:
   190  		values[Minutes] = float64((secs % 3600) / 60)
   191  	case m && s:
   192  		values[Minutes] = float64(secs / 60)
   193  	case m && h:
   194  		values[Minutes] = (float64(secs%3600) / 60) + (float64(nsec) / 6e10)
   195  	case m:
   196  		values[Minutes] = (float64(secs) / 60) + (float64(nsec) / 6e10)
   197  	}
   198  
   199  	switch {
   200  	case len(exclusive) == 0:
   201  		if v := float64(secs%60) + (float64(nsec) / 1e9); v != 0 {
   202  			values[Seconds] = v
   203  			if h && !m {
   204  				values[Minutes] = 0
   205  			}
   206  		} else if !h && !m {
   207  			values[Seconds] = 0
   208  		}
   209  	case s && m:
   210  		values[Seconds] = float64(secs%60) + (float64(nsec) / 1e9)
   211  	case s && h:
   212  		values[Seconds] = float64(secs%3600) + (float64(nsec) / 1e9)
   213  	case s:
   214  		values[Seconds] = float64(secs) + (float64(nsec) / 1e9)
   215  	}
   216  
   217  	out := "T"
   218  	if v, ok := values[Hours]; ok {
   219  		out += strconv.FormatFloat(v, 'f', -1, 64) + "H"
   220  	}
   221  
   222  	if v, ok := values[Minutes]; ok {
   223  		out += strconv.FormatFloat(v, 'f', -1, 64) + "M"
   224  	}
   225  
   226  	if v, ok := values[Seconds]; ok {
   227  		out += strconv.FormatFloat(v, 'f', -1, 64) + "S"
   228  	}
   229  	return out, neg
   230  }
   231  
   232  func (d Duration) integers() (secs int64, nsec uint32, neg bool) {
   233  	v := new(big.Int).Abs(&d.v)
   234  	var _nsec big.Int
   235  	_secs, _ := new(big.Int).DivMod(v, bigIntSecondExtent, &_nsec)
   236  	return _secs.Int64(), uint32(_nsec.Uint64()), d.v.Cmp(bigIntNegOne) == -1
   237  }
   238  
   239  // Parse the time portion of an ISO 8601 duration.
   240  func (d *Duration) Parse(s string) error {
   241  	_, _, _, _, secs, nsec, neg, err := parseDuration(s, false, true)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	*d = makeDuration(secs, nsec, neg)
   247  	return nil
   248  }
   249  
   250  // MinDuration returns the minimum supported duration.
   251  func MinDuration() Duration {
   252  	return Duration{v: *bigIntMinInt64}
   253  }
   254  
   255  // MaxDuration returns the maximum supported duration.
   256  func MaxDuration() Duration {
   257  	return Duration{v: *bigIntMaxInt64}
   258  }
   259  
   260  func makeDuration(secs int64, nsec uint32, neg bool) Duration {
   261  	out := new(big.Int).Mul(big.NewInt(secs), bigIntSecondExtent)
   262  	out.Add(out, big.NewInt(int64(nsec)))
   263  	if neg {
   264  		out.Neg(out)
   265  	}
   266  	return Duration{v: *out}
   267  }
   268  
   269  var (
   270  	bigIntNegOne = big.NewInt(-1)
   271  
   272  	bigIntMinInt64 = new(big.Int).Lsh(big.NewInt(int64(-Second)), 63)
   273  	bigIntMaxInt64 = new(big.Int).Add(new(big.Int).Lsh(big.NewInt(int64(Second)), 63), big.NewInt(-1))
   274  
   275  	bigIntSecondExtent = big.NewInt(int64(Second))
   276  
   277  	bigFloatMicrosecondExtent = big.NewFloat(float64(Microsecond))
   278  	bigFloatMillisecondExtent = big.NewFloat(float64(Millisecond))
   279  	bigFloatSecondExtent      = big.NewFloat(float64(Second))
   280  	bigFloatMinuteExtent      = big.NewFloat(float64(Minute))
   281  	bigFloatHourExtent        = big.NewFloat(float64(Hour))
   282  )