| 1 | <!DOCTYPE html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="UTF-8"> |
| 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> |
| 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 7 | <title>Duration Converter</title> |
| 8 | <style> |
| 9 | table { |
| 10 | width: 50%; |
| 11 | border-collapse: collapse; |
| 12 | margin-top: 20px; |
| 13 | } |
| 14 | table, th, td { |
| 15 | border: 1px solid black; |
| 16 | } |
| 17 | th, td { |
| 18 | padding: 10px; |
| 19 | text-align: center; |
| 20 | } |
| 21 | .error { |
| 22 | color: red; |
| 23 | margin-top: 10px; |
| 24 | } |
| 25 | </style> |
| 26 | </head> |
| 27 | <body> |
| 28 | <h1>Duration Converter</h1> |
| 29 | <input type="text" id="durationInput" placeholder="Enter duration (e.g., 10d-12h)"> |
| 30 | <div id="errorMessage" class="error"></div> |
| 31 | |
| 32 | <table id="resultTable"> |
| 33 | <thead> |
| 34 | <tr> |
| 35 | <th>Unit</th> |
| 36 | <th>Value</th> |
| 37 | <th>Formatted</th> |
| 38 | <th>Check</th> |
| 39 | </tr> |
| 40 | </thead> |
| 41 | <tbody> |
| 42 | </tbody> |
| 43 | </table> |
| 44 | |
| 45 | <script> |
| 46 | const NSEC_PER_USEC = 1000; |
| 47 | const USEC_PER_MS = 1000; |
| 48 | const NSEC_PER_SEC = 1000000000; |
| 49 | const NSEC_PER_MS = USEC_PER_MS * NSEC_PER_USEC; |
| 50 | const NSEC_PER_MIN = NSEC_PER_SEC * 60; |
| 51 | const NSEC_PER_HOUR = NSEC_PER_MIN * 60; |
| 52 | const NSEC_PER_DAY = NSEC_PER_HOUR * 24; |
| 53 | const NSEC_PER_WEEK = NSEC_PER_DAY * 7; |
| 54 | const NSEC_PER_YEAR = NSEC_PER_DAY * 365; |
| 55 | const NSEC_PER_MONTH = NSEC_PER_DAY * 30; |
| 56 | const NSEC_PER_QUARTER = NSEC_PER_MONTH * 3; |
| 57 | |
| 58 | const units = [ |
| 59 | { unit: "ns", formatter: true, multiplier: 1 }, |
| 60 | { unit: "us", formatter: true, multiplier: NSEC_PER_USEC }, |
| 61 | { unit: "ms", formatter: true, multiplier: NSEC_PER_MS }, |
| 62 | { unit: "s", formatter: true, multiplier: NSEC_PER_SEC }, |
| 63 | { unit: "m", formatter: true, multiplier: NSEC_PER_MIN }, |
| 64 | { unit: "min", formatter: false, multiplier: NSEC_PER_MIN }, |
| 65 | { unit: "h", formatter: true, multiplier: NSEC_PER_HOUR }, |
| 66 | { unit: "d", formatter: true, multiplier: NSEC_PER_DAY }, |
| 67 | { unit: "w", formatter: false, multiplier: NSEC_PER_WEEK }, |
| 68 | { unit: "wk", formatter: false, multiplier: NSEC_PER_WEEK }, |
| 69 | { unit: "mo", formatter: true, multiplier: NSEC_PER_MONTH }, |
| 70 | { unit: "M", formatter: false, multiplier: NSEC_PER_MONTH }, |
| 71 | { unit: "q", formatter: false, multiplier: NSEC_PER_QUARTER }, |
| 72 | { unit: "y", formatter: true, multiplier: NSEC_PER_YEAR }, |
| 73 | { unit: "Y", formatter: false, multiplier: NSEC_PER_YEAR }, |
| 74 | { unit: "a", formatter: false, multiplier: NSEC_PER_YEAR } |
| 75 | ]; |
| 76 | |
| 77 | function durationFindUnit(unit) { |
| 78 | if (!unit) return units[0]; |
| 79 | return units.find(u => u.unit === unit) || null; |
| 80 | } |
| 81 | |
| 82 | function roundToResolution(value, resolution) { |
| 83 | if (value > 0) return Math.floor((value + (resolution - 1) / 2) / resolution); |
| 84 | if (value < 0) return Math.ceil((value - (resolution - 1) / 2) / resolution); |
| 85 | return 0; |
| 86 | } |
| 87 | |
| 88 | function parseDouble(str) { |
| 89 | str = str.trim(); |
| 90 | const match = str.match(/^[-+]?\d*\.?\d+/); |
| 91 | if (match) { |
| 92 | const number = parseFloat(match[0]); |
| 93 | const remainingStr = str.slice(match[0].length).trim(); |
| 94 | return { number, remainingStr }; |
| 95 | } |
| 96 | return { number: null, remainingStr: str }; |
| 97 | } |
| 98 | |
| 99 | function durationParse(duration, unit) { |
| 100 | if (!duration || !unit) return false; |
| 101 | |
| 102 | let s = duration.trim(); |
| 103 | let nsec = 0; |
| 104 | let isNegative = false; |
| 105 | |
| 106 | // Handle leading negative sign |
| 107 | if (s.startsWith("-")) { |
| 108 | isNegative = true; |
| 109 | s = s.slice(1).trim(); |
| 110 | } |
| 111 | |
| 112 | while (s.length > 0) { |
| 113 | s = s.trim(); |
| 114 | |
| 115 | if (s.startsWith("never") || s.startsWith("off")) |
| 116 | return 0; |
| 117 | |
| 118 | const { number, remainingStr } = parseDouble(s); |
| 119 | if (number === null) return false; |
| 120 | |
| 121 | s = remainingStr; |
| 122 | |
| 123 | const match = s.match(/^([a-zA-Z]*)/); |
| 124 | let currentUnit = unit; |
| 125 | if (match && match[0].length > 0) { |
| 126 | currentUnit = match[0]; |
| 127 | s = s.slice(match[0].length).trim(); |
| 128 | } |
| 129 | |
| 130 | const du = durationFindUnit(currentUnit); |
| 131 | if (!du) return false; |
| 132 | |
| 133 | nsec += number * du.multiplier; |
| 134 | } |
| 135 | |
| 136 | const unitMultiplier = durationFindUnit(unit).multiplier; |
| 137 | nsec = roundToResolution(nsec, unitMultiplier); |
| 138 | |
| 139 | return isNegative ? -nsec : nsec; |
| 140 | } |
| 141 | |
| 142 | function durationSnprintf(value, unit) { |
| 143 | if (value === 0) return "off"; |
| 144 | |
| 145 | const duMin = durationFindUnit(unit); |
| 146 | let nsec = Math.abs(value) * duMin.multiplier; |
| 147 | |
| 148 | const isNegative = value < 0; |
| 149 | let result = isNegative ? "-" : ""; |
| 150 | |
| 151 | for (let i = units.length - 1; i >= 0 && nsec !== 0; i--) { |
| 152 | const du = units[i]; |
| 153 | if (!du.formatter && du !== duMin) continue; |
| 154 | |
| 155 | const multiplier = du.multiplier; |
| 156 | const rounded = (du === duMin) ? roundToResolution(nsec, multiplier) * multiplier : nsec; |
| 157 | let unitCount = Math.floor(rounded / multiplier); |
| 158 | |
| 159 | if (unitCount !== 0) { |
| 160 | result += `${unitCount}${du.unit}`; |
| 161 | nsec -= unitCount * multiplier; |
| 162 | } |
| 163 | |
| 164 | if (du === duMin) break; |
| 165 | } |
| 166 | |
| 167 | return result || "off"; |
| 168 | } |
| 169 | |
| 170 | function updateTable() { |
| 171 | const duration = document.getElementById("durationInput").value; |
| 172 | const tableBody = document.getElementById("resultTable").querySelector("tbody"); |
| 173 | const errorMessage = document.getElementById("errorMessage"); |
| 174 | tableBody.innerHTML = ""; |
| 175 | errorMessage.textContent = ""; |
| 176 | |
| 177 | units.forEach(unit => { |
| 178 | let value = durationParse(duration, unit.unit); |
| 179 | let formatted; |
| 180 | let check; |
| 181 | if(value === false) { |
| 182 | value = "-"; |
| 183 | formatted = ""; |
| 184 | check = "parsing error"; |
| 185 | } |
| 186 | else { |
| 187 | formatted = durationSnprintf(value, unit.unit); |
| 188 | const parsedValue = durationParse(formatted, unit.unit); |
| 189 | check = (parsedValue === value) ? "ok" : `re-parsing error (${parsedValue})`; |
| 190 | } |
| 191 | |
| 192 | const row = `<tr> |
| 193 | <td>${unit.unit}</td> |
| 194 | <td>${value}</td> |
| 195 | <td>${formatted}</td> |
| 196 | <td>${check}</td> |
| 197 | </tr>`; |
| 198 | tableBody.innerHTML += row; |
| 199 | }); |
| 200 | } |
| 201 | |
| 202 | document.getElementById("durationInput").addEventListener("input", updateTable); |
| 203 | </script> |
| 204 | </body> |
| 205 | </html> |