github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbtime/long_duration.go (about)

     1  package kbtime
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  )
    10  
    11  // see go/src/time/format.go `var unitMap = map[string]int64`
    12  var durationRxp = regexp.MustCompile(`^([0-9]+)\s?(([nuµμm]?s)|[mhdDMyY])$`)
    13  
    14  // There is an ambiguity with 'm' being minutes and 'M' being months. So just
    15  // don't allow units like 'd' for days - expect uppercase units for all of the
    16  // "longer durations".
    17  
    18  // AddLongDuration parses time duration from `duration` argument and adds to
    19  // time in `now`, returning the resulting time. The duration format is similar
    20  // to `time` package duration format, with the following changes:
    21  // - additional duration units are supported:
    22  //   - 'D' for days,
    23  //   - 'M' for months,
    24  //   - 'Y' for years,
    25  //   - fractional numbers are *not* supported,
    26  //   - negative numbers are *not* supported,
    27  //   - whitespace at the beginning and end of duration string is ignored,
    28  //   - optionally there can be one whitespace character between the number
    29  //     and unit.
    30  //
    31  // Long durations are handled using Time.AddDate function, which works by
    32  // adding given number of years, months, and days to tval. It normalizes its
    33  // result, for example, adding one month to October 31 yields December 1, the
    34  // normalized form for November 31.
    35  //
    36  // Examples:
    37  //
    38  //	`AddLongDuration(time.Now(), "1000 Y")`
    39  //	`AddLongDuration(time.Now(), "7 D")`
    40  //	`AddLongDuration(then, "1 M")`
    41  func AddLongDuration(tval time.Time, duration string) (ret time.Time, err error) {
    42  	duration = strings.TrimSpace(duration)
    43  
    44  	parsed := durationRxp.FindStringSubmatch(duration)
    45  	if parsed == nil {
    46  		return ret, fmt.Errorf("bad duration format %q", duration)
    47  	}
    48  
    49  	amount, err := strconv.ParseInt(parsed[1], 10, 32)
    50  	if err != nil {
    51  		return ret, fmt.Errorf("failed to parse number: %w", err)
    52  	}
    53  
    54  	unit := parsed[2]
    55  	switch unit {
    56  	case "ns", "us", "µs", "μs", "ms", "s", "m", "h":
    57  		dur, err := time.ParseDuration(fmt.Sprintf("%d%s", amount, unit))
    58  		if err != nil {
    59  			return ret, err
    60  		}
    61  		return tval.Add(dur), nil
    62  	case "d":
    63  		return ret, fmt.Errorf("use 'D' unit for days instead of '%s'", unit)
    64  	case "D": // day
    65  		return tval.AddDate(0, 0, int(amount)), nil
    66  	// 'm' is minute, handled by time.ParseDuration.
    67  	case "M": // month
    68  		return tval.AddDate(0, int(amount), 0), nil
    69  	case "y":
    70  		return ret, fmt.Errorf("use 'Y' unit for years instead of '%s'", unit)
    71  	case "Y": // year
    72  		return tval.AddDate(int(amount), 0, 0), nil
    73  	default:
    74  		return ret, fmt.Errorf("unhandled unit %q", unit)
    75  	}
    76  }