...

Source file src/github.com/sosodev/duration/duration.go

Documentation: github.com/sosodev/duration

     1  package duration
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math"
     7  	"strconv"
     8  	"time"
     9  	"unicode"
    10  )
    11  
    12  // Duration holds all the smaller units that make up the duration
    13  type Duration struct {
    14  	Years    float64
    15  	Months   float64
    16  	Weeks    float64
    17  	Days     float64
    18  	Hours    float64
    19  	Minutes  float64
    20  	Seconds  float64
    21  	Negative bool
    22  }
    23  
    24  const (
    25  	parsingPeriod = iota
    26  	parsingTime
    27  
    28  	hoursPerDay   = 24
    29  	hoursPerWeek  = hoursPerDay * 7
    30  	hoursPerMonth = hoursPerYear / 12
    31  	hoursPerYear  = hoursPerDay * 365
    32  
    33  	nsPerSecond = 1000000000
    34  	nsPerMinute = nsPerSecond * 60
    35  	nsPerHour   = nsPerMinute * 60
    36  	nsPerDay    = nsPerHour * hoursPerDay
    37  	nsPerWeek   = nsPerHour * hoursPerWeek
    38  	nsPerMonth  = nsPerHour * hoursPerMonth
    39  	nsPerYear   = nsPerHour * hoursPerYear
    40  )
    41  
    42  var (
    43  	// ErrUnexpectedInput is returned when an input in the duration string does not match expectations
    44  	ErrUnexpectedInput = errors.New("unexpected input")
    45  )
    46  
    47  // Parse attempts to parse the given duration string into a *Duration,
    48  // if parsing fails an error is returned instead
    49  func Parse(d string) (*Duration, error) {
    50  	state := parsingPeriod
    51  	duration := &Duration{}
    52  	num := ""
    53  	var err error
    54  
    55  	for _, char := range d {
    56  		switch char {
    57  		case '-':
    58  			duration.Negative = true
    59  		case 'P':
    60  			state = parsingPeriod
    61  		case 'T':
    62  			state = parsingTime
    63  		case 'Y':
    64  			if state != parsingPeriod {
    65  				return nil, ErrUnexpectedInput
    66  			}
    67  
    68  			duration.Years, err = strconv.ParseFloat(num, 64)
    69  			if err != nil {
    70  				return nil, err
    71  			}
    72  			num = ""
    73  		case 'M':
    74  			if state == parsingPeriod {
    75  				duration.Months, err = strconv.ParseFloat(num, 64)
    76  				if err != nil {
    77  					return nil, err
    78  				}
    79  				num = ""
    80  			} else if state == parsingTime {
    81  				duration.Minutes, err = strconv.ParseFloat(num, 64)
    82  				if err != nil {
    83  					return nil, err
    84  				}
    85  				num = ""
    86  			}
    87  		case 'W':
    88  			if state != parsingPeriod {
    89  				return nil, ErrUnexpectedInput
    90  			}
    91  
    92  			duration.Weeks, err = strconv.ParseFloat(num, 64)
    93  			if err != nil {
    94  				return nil, err
    95  			}
    96  			num = ""
    97  		case 'D':
    98  			if state != parsingPeriod {
    99  				return nil, ErrUnexpectedInput
   100  			}
   101  
   102  			duration.Days, err = strconv.ParseFloat(num, 64)
   103  			if err != nil {
   104  				return nil, err
   105  			}
   106  			num = ""
   107  		case 'H':
   108  			if state != parsingTime {
   109  				return nil, ErrUnexpectedInput
   110  			}
   111  
   112  			duration.Hours, err = strconv.ParseFloat(num, 64)
   113  			if err != nil {
   114  				return nil, err
   115  			}
   116  			num = ""
   117  		case 'S':
   118  			if state != parsingTime {
   119  				return nil, ErrUnexpectedInput
   120  			}
   121  
   122  			duration.Seconds, err = strconv.ParseFloat(num, 64)
   123  			if err != nil {
   124  				return nil, err
   125  			}
   126  			num = ""
   127  		default:
   128  			if unicode.IsNumber(char) || char == '.' {
   129  				num += string(char)
   130  				continue
   131  			}
   132  
   133  			return nil, ErrUnexpectedInput
   134  		}
   135  	}
   136  
   137  	return duration, nil
   138  }
   139  
   140  // FromTimeDuration converts the given time.Duration into duration.Duration.
   141  // Note that for *Duration's with period values of a month or year that the duration becomes a bit fuzzy
   142  // since obviously those things vary month to month and year to year
   143  func FromTimeDuration(d time.Duration) *Duration {
   144  	duration := &Duration{}
   145  	if d == 0 {
   146  		return duration
   147  	}
   148  
   149  	if d < 0 {
   150  		d = -d
   151  		duration.Negative = true
   152  	}
   153  
   154  	if d.Hours() >= hoursPerYear {
   155  		duration.Years = math.Floor(d.Hours() / hoursPerYear)
   156  		d -= time.Duration(duration.Years) * nsPerYear
   157  	}
   158  	if d.Hours() >= hoursPerMonth {
   159  		duration.Months = math.Floor(d.Hours() / hoursPerMonth)
   160  		d -= time.Duration(duration.Months) * nsPerMonth
   161  	}
   162  	if d.Hours() >= hoursPerWeek {
   163  		duration.Weeks = math.Floor(d.Hours() / hoursPerWeek)
   164  		d -= time.Duration(duration.Weeks) * nsPerWeek
   165  	}
   166  	if d.Hours() >= hoursPerDay {
   167  		duration.Days = math.Floor(d.Hours() / hoursPerDay)
   168  		d -= time.Duration(duration.Days) * nsPerDay
   169  	}
   170  	if d.Hours() >= 1 {
   171  		duration.Hours = math.Floor(d.Hours())
   172  		d -= time.Duration(duration.Hours) * nsPerHour
   173  	}
   174  	if d.Minutes() >= 1 {
   175  		duration.Minutes = math.Floor(d.Minutes())
   176  		d -= time.Duration(duration.Minutes) * nsPerMinute
   177  	}
   178  	duration.Seconds = d.Seconds()
   179  
   180  	return duration
   181  }
   182  
   183  // Format formats the given time.Duration into an ISO 8601 duration string (e.g. P1DT6H5M),
   184  // negative durations are prefixed with a minus sign, for a zero duration "PT0S" is returned.
   185  // Note that for *Duration's with period values of a month or year that the duration becomes a bit fuzzy
   186  // since obviously those things vary month to month and year to year
   187  func Format(d time.Duration) string {
   188  	return FromTimeDuration(d).String()
   189  }
   190  
   191  // ToTimeDuration converts the *Duration to the standard library's time.Duration.
   192  // Note that for *Duration's with period values of a month or year that the duration becomes a bit fuzzy
   193  // since obviously those things vary month to month and year to year
   194  func (duration *Duration) ToTimeDuration() time.Duration {
   195  	var timeDuration time.Duration
   196  
   197  	// zero checks are here to avoid unnecessary math operations, on a durations such as `PT5M`
   198  	if duration.Years != 0 {
   199  		timeDuration += time.Duration(math.Round(duration.Years * nsPerYear))
   200  	}
   201  	if duration.Months != 0 {
   202  		timeDuration += time.Duration(math.Round(duration.Months * nsPerMonth))
   203  	}
   204  	if duration.Weeks != 0 {
   205  		timeDuration += time.Duration(math.Round(duration.Weeks * nsPerWeek))
   206  	}
   207  	if duration.Days != 0 {
   208  		timeDuration += time.Duration(math.Round(duration.Days * nsPerDay))
   209  	}
   210  	if duration.Hours != 0 {
   211  		timeDuration += time.Duration(math.Round(duration.Hours * nsPerHour))
   212  	}
   213  	if duration.Minutes != 0 {
   214  		timeDuration += time.Duration(math.Round(duration.Minutes * nsPerMinute))
   215  	}
   216  	if duration.Seconds != 0 {
   217  		timeDuration += time.Duration(math.Round(duration.Seconds * nsPerSecond))
   218  	}
   219  	if duration.Negative {
   220  		timeDuration = -timeDuration
   221  	}
   222  
   223  	return timeDuration
   224  }
   225  
   226  // String returns the ISO8601 duration string for the *Duration
   227  func (duration *Duration) String() string {
   228  	d := "P"
   229  	hasTime := false
   230  
   231  	appendD := func(designator string, value float64, isTime bool) {
   232  		if !hasTime && isTime {
   233  			d += "T"
   234  			hasTime = true
   235  		}
   236  
   237  		d += strconv.FormatFloat(value, 'f', -1, 64) + designator
   238  	}
   239  
   240  	if duration.Years != 0 {
   241  		appendD("Y", duration.Years, false)
   242  	}
   243  
   244  	if duration.Months != 0 {
   245  		appendD("M", duration.Months, false)
   246  	}
   247  
   248  	if duration.Weeks != 0 {
   249  		appendD("W", duration.Weeks, false)
   250  	}
   251  
   252  	if duration.Days != 0 {
   253  		appendD("D", duration.Days, false)
   254  	}
   255  
   256  	if duration.Hours != 0 {
   257  		appendD("H", duration.Hours, true)
   258  	}
   259  
   260  	if duration.Minutes != 0 {
   261  		appendD("M", duration.Minutes, true)
   262  	}
   263  
   264  	if duration.Seconds != 0 {
   265  		appendD("S", duration.Seconds, true)
   266  	}
   267  
   268  	// if the duration is zero, return "PT0S"
   269  	if d == "P" {
   270  		d += "T0S"
   271  	}
   272  
   273  	if duration.Negative {
   274  		return "-" + d
   275  	}
   276  
   277  	return d
   278  }
   279  
   280  func (duration Duration) MarshalJSON() ([]byte, error) {
   281  	return []byte("\"" + duration.String() + "\""), nil
   282  }
   283  
   284  func (duration *Duration) UnmarshalJSON(source []byte) error {
   285  	strVal := string(source)
   286  	if len(strVal) < 2 {
   287  		return fmt.Errorf("invalid ISO 8601 duration: %s", strVal)
   288  	}
   289  	strVal = strVal[1 : len(strVal)-1]
   290  
   291  	if strVal == "null" {
   292  		return nil
   293  	}
   294  
   295  	parsed, err := Parse(strVal)
   296  	if err != nil {
   297  		return fmt.Errorf("invalid ISO 8601 duration: %s", strVal)
   298  	}
   299  	*duration = *parsed
   300  	return nil
   301  }
   302  

View as plain text