postgres-interval 4.0.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +218 -111
  2. package/package.json +2 -2
  3. package/readme.md +2 -0
package/index.js CHANGED
@@ -7,34 +7,71 @@ function PostgresInterval (raw) {
7
7
  return new PostgresInterval(raw)
8
8
  }
9
9
 
10
- Object.assign(this, parse(raw))
10
+ this.years = 0
11
+ this.months = 0
12
+ this.days = 0
13
+ this.hours = 0
14
+ this.minutes = 0
15
+ this.seconds = 0
16
+ this.milliseconds = 0
17
+
18
+ parse(this, raw)
11
19
  }
12
- const properties = ['years', 'months', 'days', 'hours', 'minutes', 'seconds']
20
+
13
21
  PostgresInterval.prototype.toPostgres = function () {
14
- const filtered = properties.filter(key => Object.prototype.hasOwnProperty.call(this, key) && this[key] !== 0)
22
+ let postgresString = ''
15
23
 
16
- // In addition to `properties`, we need to account for fractions of seconds.
17
- if (this.milliseconds && !filtered.includes('seconds')) {
18
- filtered.push('seconds')
24
+ if (this.years) {
25
+ postgresString += this.years === 1 ? this.years + ' year' : this.years + ' years'
19
26
  }
20
27
 
21
- if (filtered.length === 0) return '0'
22
- return filtered
23
- .map(function (property) {
24
- let value = this[property] || 0
28
+ if (this.months) {
29
+ if (postgresString.length) {
30
+ postgresString += ' '
31
+ }
25
32
 
26
- // Account for fractional part of seconds,
27
- // remove trailing zeroes.
28
- if (property === 'seconds' && this.milliseconds) {
29
- value = (value + this.milliseconds / 1000).toFixed(6).replace(/\.?0+$/, '')
30
- }
33
+ postgresString += this.months === 1 ? this.months + ' month' : this.months + ' months'
34
+ }
35
+
36
+ if (this.days) {
37
+ if (postgresString.length) {
38
+ postgresString += ' '
39
+ }
40
+
41
+ postgresString += this.days === 1 ? this.days + ' day' : this.days + ' days'
42
+ }
43
+
44
+ if (this.hours) {
45
+ if (postgresString.length) {
46
+ postgresString += ' '
47
+ }
48
+
49
+ postgresString += this.hours === 1 ? this.hours + ' hour' : this.hours + ' hours'
50
+ }
51
+
52
+ if (this.minutes) {
53
+ if (postgresString.length) {
54
+ postgresString += ' '
55
+ }
56
+
57
+ postgresString += this.minutes === 1 ? this.minutes + ' minute' : this.minutes + ' minutes'
58
+ }
59
+
60
+ if (this.seconds || this.milliseconds) {
61
+ if (postgresString.length) {
62
+ postgresString += ' '
63
+ }
64
+
65
+ if (this.milliseconds) {
66
+ const value = Math.trunc((this.seconds + this.milliseconds / 1000) * 1000000) / 1000000
67
+
68
+ postgresString += value === 1 ? value + ' second' : value + ' seconds'
69
+ } else {
70
+ postgresString += this.seconds === 1 ? this.seconds + ' second' : this.seconds + ' seconds'
71
+ }
72
+ }
31
73
 
32
- // fractional seconds will be a String, all others are Number
33
- const isSingular = String(value) === '1'
34
- // Remove plural 's' when the value is singular
35
- return value + ' ' + (isSingular ? property.replace(/s$/, '') : property)
36
- }, this)
37
- .join(' ')
74
+ return postgresString === '' ? '0' : postgresString
38
75
  }
39
76
 
40
77
  const propertiesISOEquivalent = {
@@ -45,115 +82,185 @@ const propertiesISOEquivalent = {
45
82
  minutes: 'M',
46
83
  seconds: 'S'
47
84
  }
48
- const dateProperties = ['years', 'months', 'days']
49
- const timeProperties = ['hours', 'minutes', 'seconds']
85
+
50
86
  // according to ISO 8601
51
- PostgresInterval.prototype.toISOString = PostgresInterval.prototype.toISO = function () {
52
- return toISOString.call(this, { short: false })
53
- }
87
+ PostgresInterval.prototype.toISOString = PostgresInterval.prototype.toISO =
88
+ function () {
89
+ return toISOString.call(this, { short: false })
90
+ }
54
91
 
55
92
  PostgresInterval.prototype.toISOStringShort = function () {
56
93
  return toISOString.call(this, { short: true })
57
94
  }
58
95
 
59
- function toISOString ({ short = false }) {
60
- const datePart = dateProperties
61
- .map(buildProperty, this)
62
- .join('')
96
+ function toISOString ({ short }) {
97
+ let datePart = ''
98
+
99
+ if (!short || this.years) {
100
+ datePart += this.years + propertiesISOEquivalent.years
101
+ }
63
102
 
64
- const timePart = timeProperties
65
- .map(buildProperty, this)
66
- .join('')
103
+ if (!short || this.months) {
104
+ datePart += this.months + propertiesISOEquivalent.months
105
+ }
67
106
 
68
- if (!timePart.length && !datePart.length) return 'PT0S'
107
+ if (!short || this.days) {
108
+ datePart += this.days + propertiesISOEquivalent.days
109
+ }
69
110
 
70
- if (!timePart.length) return `P${datePart}`
111
+ let timePart = ''
112
+
113
+ if (!short || this.hours) {
114
+ timePart += this.hours + propertiesISOEquivalent.hours
115
+ }
116
+
117
+ if (!short || this.minutes) {
118
+ timePart += this.minutes + propertiesISOEquivalent.minutes
119
+ }
120
+
121
+ if (!short || (this.seconds || this.milliseconds)) {
122
+ if (this.milliseconds) {
123
+ timePart += (Math.trunc((this.seconds + this.milliseconds / 1000) * 1000000) / 1000000) + propertiesISOEquivalent.seconds
124
+ } else {
125
+ timePart += this.seconds + propertiesISOEquivalent.seconds
126
+ }
127
+ }
128
+
129
+ if (!timePart && !datePart) {
130
+ return 'PT0S'
131
+ }
132
+
133
+ if (!timePart) {
134
+ return `P${datePart}`
135
+ }
71
136
 
72
137
  return `P${datePart}T${timePart}`
138
+ }
139
+
140
+ const position = { value: 0 }
73
141
 
74
- function buildProperty (property) {
75
- let value = this[property] || 0
142
+ function readNextNum (interval) {
143
+ let val = 0
76
144
 
77
- // Account for fractional part of seconds,
78
- // remove trailing zeroes.
79
- if (property === 'seconds' && this.milliseconds) {
80
- value = (value + this.milliseconds / 1000).toFixed(6).replace(/0+$/, '')
145
+ while (position.value < interval.length) {
146
+ const char = interval[position.value]
147
+
148
+ if (char >= '0' && char <= '9') {
149
+ val = val * 10 + +char
150
+ position.value++
151
+ } else {
152
+ break
81
153
  }
154
+ }
82
155
 
83
- if (short && !value) return ''
156
+ return val
157
+ }
158
+
159
+ function parseMillisecond (interval) {
160
+ const previousPosition = position.value
161
+ const currentValue = readNextNum(interval)
162
+ const valueStringLength = position.value - previousPosition
84
163
 
85
- return value + propertiesISOEquivalent[property]
164
+ switch (valueStringLength) {
165
+ case 1:
166
+ return currentValue * 100
167
+ case 2:
168
+ return currentValue * 10
169
+ case 3:
170
+ return currentValue
171
+ case 4:
172
+ return currentValue / 10
173
+ case 5:
174
+ return currentValue / 100
175
+ case 6:
176
+ return currentValue / 1000
86
177
  }
178
+
179
+ // slow path
180
+ const remainder = valueStringLength - 3
181
+ return currentValue / Math.pow(10, remainder)
87
182
  }
88
183
 
89
- const NUMBER = '([+-]?\\d+)'
90
- const YEAR = `${NUMBER}\\s+years?`
91
- const MONTH = `${NUMBER}\\s+mons?`
92
- const DAY = `${NUMBER}\\s+days?`
93
- // NOTE: PostgreSQL automatically overflows seconds into minutes and minutes
94
- // into hours, so we can rely on minutes and seconds always being 2 digits
95
- // (plus decimal for seconds). The overflow stops at hours - hours do not
96
- // overflow into days, so could be arbitrarily long.
97
- const TIME = '([+-])?(\\d+):(\\d\\d):(\\d\\d(?:\\.\\d{1,6})?)'
98
- const INTERVAL = new RegExp(
99
- '^\\s*' +
100
- // All parts of an interval are optional
101
- [YEAR, MONTH, DAY, TIME].map((str) => '(?:' + str + ')?').join('\\s*') +
102
- '\\s*$'
103
- )
104
-
105
- // All intervals will have exactly these properties:
106
- const ZERO_INTERVAL = Object.freeze({
107
- years: 0,
108
- months: 0,
109
- days: 0,
110
- hours: 0,
111
- minutes: 0,
112
- seconds: 0,
113
- milliseconds: 0.0
114
- })
115
-
116
- function parse (interval) {
184
+ function parse (instance, interval) {
117
185
  if (!interval) {
118
- return ZERO_INTERVAL
119
- }
120
-
121
- const matches = INTERVAL.exec(interval) || []
122
-
123
- const [
124
- ,
125
- yearsString,
126
- monthsString,
127
- daysString,
128
- plusMinusTime,
129
- hoursString,
130
- minutesString,
131
- secondsString
132
- ] = matches
133
-
134
- const timeMultiplier = plusMinusTime === '-' ? -1 : 1
135
-
136
- const years = yearsString ? parseInt(yearsString, 10) : 0
137
- const months = monthsString ? parseInt(monthsString, 10) : 0
138
- const days = daysString ? parseInt(daysString, 10) : 0
139
- const hours = hoursString ? timeMultiplier * parseInt(hoursString, 10) : 0
140
- const minutes = minutesString
141
- ? timeMultiplier * parseInt(minutesString, 10)
142
- : 0
143
- const secondsFloat = parseFloat(secondsString) || 0
144
- // secondsFloat is guaranteed to be >= 0, so floor is safe
145
- const absSeconds = Math.floor(secondsFloat)
146
- const seconds = timeMultiplier * absSeconds
147
- // Without the rounding, we end up with decimals like 455.99999999999994 instead of 456
148
- const milliseconds = Math.round(timeMultiplier * (secondsFloat - absSeconds) * 1000000) / 1000
149
- return {
150
- years,
151
- months,
152
- days,
153
- hours,
154
- minutes,
155
- seconds,
156
- milliseconds
186
+ return
157
187
  }
188
+
189
+ position.value = 0
190
+
191
+ let currentValue
192
+ let nextNegative = 1
193
+
194
+ while (position.value < interval.length) {
195
+ const char = interval[position.value]
196
+
197
+ if (char === '-') {
198
+ nextNegative = -1
199
+ position.value++
200
+ continue
201
+ } else if (char === '+') {
202
+ position.value++
203
+ continue
204
+ } else if (char === ' ') {
205
+ position.value++
206
+ continue
207
+ } else if (char < '0' || char > '9') {
208
+ position.value++
209
+ continue
210
+ } else {
211
+ currentValue = readNextNum(interval)
212
+
213
+ if (interval[position.value] === ':') {
214
+ instance.hours = currentValue ? nextNegative * currentValue : 0
215
+
216
+ position.value++
217
+ currentValue = readNextNum(interval)
218
+ instance.minutes = currentValue ? nextNegative * currentValue : 0
219
+
220
+ position.value++
221
+ currentValue = readNextNum(interval)
222
+ instance.seconds = currentValue ? nextNegative * currentValue : 0
223
+
224
+ if (interval[position.value] === '.') {
225
+ position.value++
226
+
227
+ currentValue = parseMillisecond(interval)
228
+ instance.milliseconds = currentValue ? nextNegative * currentValue : 0
229
+ }
230
+
231
+ return
232
+ }
233
+
234
+ // skip space
235
+ position.value++
236
+
237
+ const unit = interval[position.value]
238
+
239
+ if (unit === 'y') {
240
+ instance.years = currentValue ? nextNegative * currentValue : 0
241
+ } else if (unit === 'm') {
242
+ instance.months = currentValue ? nextNegative * currentValue : 0
243
+ } else if (unit === 'd') {
244
+ instance.days = currentValue ? nextNegative * currentValue : 0
245
+ }
246
+
247
+ nextNegative = 1
248
+ }
249
+ }
250
+ }
251
+
252
+ PostgresInterval.parse = function (interval) {
253
+ const instance = {
254
+ years: 0,
255
+ months: 0,
256
+ days: 0,
257
+ hours: 0,
258
+ minutes: 0,
259
+ seconds: 0,
260
+ milliseconds: 0
261
+ }
262
+
263
+ parse(instance, interval)
264
+
265
+ return instance
158
266
  }
159
- PostgresInterval.parse = parse
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgres-interval",
3
3
  "main": "index.js",
4
- "version": "4.0.0",
4
+ "version": "4.0.2",
5
5
  "description": "Parse Postgres interval columns",
6
6
  "license": "MIT",
7
7
  "repository": "bendrucker/postgres-interval",
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "dependencies": {},
25
25
  "devDependencies": {
26
- "standard": "^16.0.0",
26
+ "standard": "^17.0.0",
27
27
  "tape": "^5.0.0"
28
28
  },
29
29
  "files": [
package/readme.md CHANGED
@@ -41,6 +41,8 @@ Type: `string`
41
41
 
42
42
  A Postgres interval string.
43
43
 
44
+ This package is focused on parsing Postgres outputs. It optimizes for performance by assuming that inputs follow the default interval format. It does not perform any validation on the input. If any interval field is not found, its value will be set to `0` in the returned `interval`.
45
+
44
46
  #### `interval.toPostgres()` -> `string`
45
47
 
46
48
  Returns an interval string. This allows the interval object to be passed into prepared statements.