Jiri Simsa | 5293dcb | 2014-05-10 09:56:38 -0700 | [diff] [blame] | 1 | package unit |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "math" |
| 6 | "regexp" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | ) |
| 10 | |
| 11 | // metersInFoot denotes the number of meters in one foot, which is *exactly* |
| 12 | // 0.3048 (i.e., there is no rounding). |
| 13 | const ( |
| 14 | metersInFoot = 0.3048 |
| 15 | // MaxDistance represents the largest expressable distance. |
| 16 | MaxDistance Distance = math.MaxFloat64 |
| 17 | ) |
| 18 | |
| 19 | // Distance represents a physical distance with floating-point precision. |
| 20 | // It can be expressed in both metric and imperial units. |
| 21 | // |
| 22 | // Distance is internally stored in millimeter units. |
| 23 | type Distance float64 |
| 24 | |
| 25 | // Common distances. To convert an integer number of units into a Distance, |
| 26 | // multiply: |
| 27 | // fmt.Print(5*unit.Meter) // prints "5m0mm" |
| 28 | // |
| 29 | // To count a number of units in a Distance, divide: |
| 30 | // d := 5*unit.Meter |
| 31 | // fmt.Print(int64(d/unit.Centimeter)) // prints 500 |
| 32 | // |
| 33 | // Distances can be added or subtracted: |
| 34 | // fmt.Print(5*unit.Meter + 3*unit.Centimeter) // prints "5m30mm" |
| 35 | // fmt.Print(3*unit.Kilometer - 2*unit.Kilometer) // prints "1km0m0mm" |
| 36 | const ( |
| 37 | Millimeter Distance = 1 |
| 38 | Centimeter = 10 * Millimeter |
| 39 | Meter = 1000 * Millimeter |
| 40 | Kilometer = 1000 * Meter |
| 41 | Foot = metersInFoot * Meter |
| 42 | Inch = Foot / 12 |
| 43 | Yard = 3 * Foot |
| 44 | Mile = 1760 * Yard |
| 45 | ) |
| 46 | |
| 47 | var parseRegexp = regexp.MustCompile("^([0-9\\+\\-\\.eE]*)([mM][iI]|[kK][mM]|[mM][mM]|[cC][mM]|[fF][tT]|[iI][nN]|[fF][tT]|[yY][dD]|[mM])") |
| 48 | |
| 49 | // Parse parses the provided string and returns a new Distance. The provided |
| 50 | // string must be of the following format: |
| 51 | // |
| 52 | // [+-]*(<positive_float_num>("mi"|"km"|"m"|"yd"|"ft"|"in"|"cm"|"mm"))+ |
| 53 | // |
| 54 | // For example, the following are legitimate distance strings: |
| 55 | // |
| 56 | // "10.2mi8km0.3m12yd100ft18in12.82cm22mm" |
| 57 | // "10km12m88cm" |
| 58 | // "12yd8ft3in" |
| 59 | // "-12yd3.2ft" |
| 60 | // |
| 61 | // ParseDistance will return an error if the string doesn't conform to the |
| 62 | // above format. |
| 63 | func ParseDistance(value string) (Distance, error) { |
| 64 | var total Distance |
| 65 | s := value |
| 66 | |
| 67 | // See if the string starts with a +- sign and if so consume it. |
| 68 | neg := false |
| 69 | if len(s) > 0 && s[0] == '+' { |
| 70 | s = s[1:] |
| 71 | } else if len(s) > 0 && s[0] == '-' { |
| 72 | s = s[1:] |
| 73 | neg = true |
| 74 | } |
| 75 | match := parseRegexp.FindStringSubmatch(s) |
| 76 | if match == nil { |
| 77 | return 0, fmt.Errorf("invalid distance string: %q", value) |
| 78 | } |
| 79 | for match != nil && len(match) == 3 { |
| 80 | full, value, unit := match[0], match[1], match[2] |
| 81 | // Parse value. |
| 82 | f, err := strconv.ParseFloat(value, 64) |
| 83 | if err != nil { |
| 84 | return 0, fmt.Errorf("error parsing float in distance string %q: %s", value, err) |
| 85 | } |
| 86 | if f < 0 { |
| 87 | return 0, fmt.Errorf("can't use negative float unit count in distance string %q: %f", value, f) |
| 88 | } |
| 89 | // Parse unit. |
| 90 | var mult Distance |
| 91 | switch strings.ToLower(unit) { |
| 92 | case "mi": |
| 93 | mult = Mile |
| 94 | case "km": |
| 95 | mult = Kilometer |
| 96 | case "m": |
| 97 | mult = Meter |
| 98 | case "yd": |
| 99 | mult = Yard |
| 100 | case "ft": |
| 101 | mult = Foot |
| 102 | case "in": |
| 103 | mult = Inch |
| 104 | case "cm": |
| 105 | mult = Centimeter |
| 106 | case "mm": |
| 107 | mult = Millimeter |
| 108 | default: // should never happen |
| 109 | return 0, fmt.Errorf("illegal unit in distance string %q: %q", value, unit) |
| 110 | } |
| 111 | // Add the newly parsed sub-distance to the total. |
| 112 | total += Distance(f) * mult |
| 113 | // Advance to the next matched sub-distance. |
| 114 | s = s[len(full):] |
| 115 | match = parseRegexp.FindStringSubmatch(s) |
| 116 | } |
| 117 | |
| 118 | if len(s) > 0 { |
| 119 | return 0, fmt.Errorf("invalid distance string: %q", value) |
| 120 | } |
| 121 | |
| 122 | if neg { |
| 123 | total *= -1 |
| 124 | } |
| 125 | return total, nil |
| 126 | } |
| 127 | |
| 128 | func (d Distance) String() string { |
| 129 | var sign string |
| 130 | if d < 0 { |
| 131 | sign = "-" |
| 132 | } |
| 133 | v := math.Abs(float64(d)) |
| 134 | kms := math.Floor(v / float64(Kilometer)) |
| 135 | v = v - kms*float64(Kilometer) |
| 136 | ms := math.Floor(v / float64(Meter)) |
| 137 | v = v - ms*float64(Meter) |
| 138 | mms := v |
| 139 | if kms > 0 { |
| 140 | return fmt.Sprintf("%s%gkm%gm%gmm", sign, kms, ms, mms) |
| 141 | } |
| 142 | if ms > 0 { |
| 143 | return fmt.Sprintf("%s%gm%gmm", sign, ms, mms) |
| 144 | } |
| 145 | return fmt.Sprintf("%s%gmm", sign, mms) |
| 146 | } |
| 147 | |
| 148 | // Miles returns a distance value in mile units. |
| 149 | func (d Distance) Miles() float64 { |
| 150 | return float64(d / Mile) |
| 151 | } |
| 152 | |
| 153 | // Kilometers returns a distance value in kilometer units. |
| 154 | func (d Distance) Kilometers() float64 { |
| 155 | return float64(d / Kilometer) |
| 156 | } |
| 157 | |
| 158 | // Meters returns a distance value in meter units. |
| 159 | func (d Distance) Meters() float64 { |
| 160 | return float64(d / Meter) |
| 161 | } |
| 162 | |
| 163 | // Yards returns a distance value in yard units. |
| 164 | func (d Distance) Yards() float64 { |
| 165 | return float64(d / Yard) |
| 166 | } |
| 167 | |
| 168 | // Feet returns a distance value in foot units. |
| 169 | func (d Distance) Feet() float64 { |
| 170 | return float64(d / Foot) |
| 171 | } |
| 172 | |
| 173 | // Inches returns a distance value in inch units. |
| 174 | func (d Distance) Inches() float64 { |
| 175 | return float64(d / Inch) |
| 176 | } |
| 177 | |
| 178 | // Centimeters returns a distance value in centimeter units. |
| 179 | func (d Distance) Centimeters() float64 { |
| 180 | return float64(d / Centimeter) |
| 181 | } |
| 182 | |
| 183 | // Millimeters returns a distance value in millimeter units. |
| 184 | func (d Distance) Millimeters() float64 { |
| 185 | return float64(d) |
| 186 | } |