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

     1  package chrono
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"strconv"
     7  	"strings"
     8  )
     9  
    10  // Period represents an amount of time in years, months, weeks and days.
    11  // A period is not a measurable quantity since the lengths of these components is ambiguous.
    12  type Period struct {
    13  	Years  float32
    14  	Months float32
    15  	Weeks  float32
    16  	Days   float32
    17  }
    18  
    19  // Equal reports whether p and p2 represent the same period of time.
    20  func (p Period) Equal(p2 Period) bool {
    21  	return p2.Years == p.Years && p2.Months == p.Months && p2.Weeks == p.Weeks && p2.Days == p.Days
    22  }
    23  
    24  // String returns a string formatted according to ISO 8601.
    25  // It is equivalent to calling [Format] with no arguments.
    26  func (p Period) String() string {
    27  	return p.Format()
    28  }
    29  
    30  // Format the duration according to ISO 8601.
    31  // The output consists of only the period component - the time component is never included.
    32  func (p Period) Format() string {
    33  	if p.Years == 0 && p.Months == 0 && p.Weeks == 0 && p.Days == 0 {
    34  		return "P0D"
    35  	}
    36  
    37  	out := "P"
    38  	if p.Years != 0 {
    39  		out += strconv.FormatFloat(math.Abs(float64(p.Years)), 'f', -1, 32) + "Y"
    40  	}
    41  
    42  	if p.Months != 0 {
    43  		out += strconv.FormatFloat(math.Abs(float64(p.Months)), 'f', -1, 32) + "M"
    44  	}
    45  
    46  	if p.Weeks != 0 {
    47  		out += strconv.FormatFloat(math.Abs(float64(p.Weeks)), 'f', -1, 32) + "W"
    48  	}
    49  
    50  	if p.Days != 0 {
    51  		out += strconv.FormatFloat(math.Abs(float64(p.Days)), 'f', -1, 32) + "D"
    52  	}
    53  	return out
    54  }
    55  
    56  // Parse the period portion of an ISO 8601 duration.
    57  // This function supports the ISO 8601-2 extension, which allows weeks (W) to appear in combination
    58  // with years, months, and days, such as P3W1D. Additionally, it allows a sign character to appear
    59  // at the start of string, such as +P1M, or -P1M.
    60  func (p *Period) Parse(s string) error {
    61  	years, months, weeks, days, _, _, _, err := parseDuration(s, true, false)
    62  	if err != nil {
    63  		return err
    64  	}
    65  
    66  	p.Years = years
    67  	p.Months = months
    68  	p.Weeks = weeks
    69  	p.Days = days
    70  	return nil
    71  }
    72  
    73  // FormatDuration formats a combined period and duration to a complete ISO 8601 duration.
    74  func FormatDuration(p Period, d Duration, exclusive ...Designator) string {
    75  	out := p.Format()
    76  
    77  	t, neg := d.format(exclusive...)
    78  	out += t
    79  
    80  	if neg {
    81  		out = "-" + out
    82  	}
    83  	return out
    84  }
    85  
    86  // ParseDuration parses a complete ISO 8601 duration.
    87  func ParseDuration(s string) (Period, Duration, error) {
    88  	years, months, weeks, days, secs, nsec, neg, err := parseDuration(s, true, true)
    89  	return Period{
    90  		Years:  years,
    91  		Months: months,
    92  		Weeks:  weeks,
    93  		Days:   days,
    94  	}, makeDuration(secs, nsec, neg), err
    95  }
    96  
    97  func parseDuration(s string, parsePeriod, parseTime bool) (years, months, weeks, days float32, secs int64, nsec uint32, neg bool, err error) {
    98  	if len(s) == 0 {
    99  		return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("empty string")
   100  	}
   101  
   102  	offset := 1
   103  	if s[0] == '+' {
   104  		offset++
   105  	} else if s[0] == '-' {
   106  		neg = true
   107  		offset++
   108  	} else if s[0] != 'P' {
   109  		return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("expecting 'P'")
   110  	}
   111  
   112  	var value int
   113  	var onTime bool
   114  	var haveUnit bool
   115  
   116  	for i := offset; i < len(s); i++ {
   117  		digit := (s[i] >= '0' && s[i] <= '9') || s[i] == '.' || s[i] == ','
   118  
   119  		if value == 0 {
   120  			if digit {
   121  				value = i
   122  			} else if s[i] == 'T' {
   123  				if !onTime {
   124  					onTime = true
   125  				} else {
   126  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting digit", s[i])
   127  				}
   128  			} else {
   129  				return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting digit or 'T'", s[i])
   130  			}
   131  		} else {
   132  			if !onTime {
   133  				if !parsePeriod {
   134  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("cannot parse duration as Duration")
   135  				} else if digit {
   136  					continue
   137  				}
   138  
   139  				v, err := parseFloat(s[value:i], 32)
   140  				if err != nil {
   141  					return 0, 0, 0, 0, 0, 0, false, err
   142  				}
   143  
   144  				switch s[i] {
   145  				case 'Y':
   146  					years = float32(v)
   147  				case 'M':
   148  					months = float32(v)
   149  				case 'W':
   150  					weeks = float32(v)
   151  				case 'D':
   152  					days = float32(v)
   153  				default:
   154  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting 'Y', 'M', 'W', or 'D'", s[i])
   155  				}
   156  
   157  				value = 0
   158  				haveUnit = true
   159  			} else {
   160  				if !parseTime {
   161  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("cannot parse duration as Period")
   162  				} else if digit {
   163  					continue
   164  				}
   165  
   166  				v, err := parseFloat(s[value:i], 64)
   167  				if err != nil {
   168  					return 0, 0, 0, 0, 0, 0, false, err
   169  				}
   170  
   171  				var _secs float64
   172  				var _nsec uint32
   173  				switch s[i] {
   174  				case 'H':
   175  					_secs = math.Floor(v * 3600)
   176  					_nsec = uint32((v * 3.6e12) - (_secs * 1e9))
   177  				case 'M':
   178  					_secs = math.Floor(v * 60)
   179  					_nsec = uint32((v * 6e10) - (_secs * 1e9))
   180  				case 'S':
   181  					_secs = math.Floor(v)
   182  					_nsec = uint32((v * 1e9) - (_secs * 1e9))
   183  				default:
   184  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("unexpected '%c', expecting 'H', 'M' or 'S'", s[i])
   185  				}
   186  
   187  				if _secs < math.MinInt64 {
   188  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow")
   189  				} else if _secs > math.MaxInt64 {
   190  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow")
   191  				}
   192  
   193  				var under, over bool
   194  				if secs, under, over = addInt64(secs, int64(_secs)); under {
   195  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow")
   196  				} else if over {
   197  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow")
   198  				}
   199  
   200  				if secs, under, over = addInt64(secs, int64(_nsec/1e9)); under {
   201  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds underflow")
   202  				} else if over {
   203  					return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("seconds overflow")
   204  				}
   205  				nsec = _nsec % 1e9
   206  
   207  				value = 0
   208  				haveUnit = true
   209  			}
   210  		}
   211  	}
   212  
   213  	if !haveUnit {
   214  		return 0, 0, 0, 0, 0, 0, false, fmt.Errorf("expecting at least one unit")
   215  	}
   216  	return
   217  }
   218  
   219  func parseFloat(s string, bitSize int) (float64, error) {
   220  	s = strings.ReplaceAll(s, ",", ".")
   221  	return strconv.ParseFloat(s, bitSize)
   222  }