Skip to content

Commit

Permalink
Refactor as loop-iteration
Browse files Browse the repository at this point in the history
  • Loading branch information
hans00 committed Aug 13, 2023
1 parent 55b2fca commit 0e32630
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 81 deletions.
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,30 @@ runtime: node v18.14.0 (x64-linux)

benchmark time (avg) (min … max) p75 p99 p995
-------------------------------------------------------- -----------------------------
faster-qs 6.1 µs/iter (5.53 µs … 432.77 µs) 5.81 µs 12.17 µs 14.35 µs
qs 36.64 µs/iter (31.87 µs … 1.12 ms) 33.45 µs 84.47 µs 108.88 µs
fast-querystring 3.27 µs/iter (2.87 µs … 5.95 µs) 3.28 µs 5.95 µs 5.95 µs
node:querystring 4.26 µs/iter (3.74 µs … 369.83 µs) 4.05 µs 7.9 µs 9.06 µs
• basic
-------------------------------------------------------- -----------------------------
faster-qs 603.21 ns/iter (547.73 ns … 1.14 µs) 607.07 ns 1.14 µs 1.14 µs
qs 6.72 µs/iter (5.28 µs … 440.78 µs) 5.86 µs 16.99 µs 25.17 µs
fast-querystring 490.43 ns/iter (393.92 ns … 1.02 µs) 455.21 ns 974.32 ns 1.02 µs
node:querystring 479.66 ns/iter (426.49 ns … 897.91 ns) 477.84 ns 814.14 ns 897.91 ns

summary for basic
faster-qs
1.26x slower than node:querystring
1.23x slower than fast-querystring
11.14x faster than qs

summary
• deep object
-------------------------------------------------------- -----------------------------
faster-qs 4.46 µs/iter (4.29 µs … 4.9 µs) 4.55 µs 4.9 µs 4.9 µs
qs 17.48 µs/iter (15.15 µs … 803.78 µs) 16.15 µs 33.99 µs 43.18 µs
fast-querystring 1.21 µs/iter (1.14 µs … 1.48 µs) 1.22 µs 1.48 µs 1.48 µs
node:querystring 1.65 µs/iter (1.54 µs … 2.05 µs) 1.65 µs 2.05 µs 2.05 µs

summary for deep object
faster-qs
1.87x slower than fast-querystring
1.43x slower than node:querystring
6x faster than qs
3.7x slower than fast-querystring
2.71x slower than node:querystring
3.92x faster than qs

```
32 changes: 19 additions & 13 deletions __tests__/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,61 @@ import chai from 'chai'
import qs from 'qs'
import parse from '../index.mjs'

const expect = (payload, target) =>
chai.expect(parse(payload)).to.be.deep.equal(target)

describe('parse', () => {
it('array', () => {
chai.expect(parse('a[]&a[]&a[]&a[]&a[]&a[]')).to.be.deep.equal({
expect('a[]&a[]&a[]&a[]&a[]&a[]', {
a: ['', '', '', '', '', ''],
})
chai.expect(parse('a&a&a&a[]&a[]&a[]')).to.be.deep.equal({
expect('a&a&a&a[]&a[]&a[]', {
a: ['', '', '', '', '', ''],
})
})

it('nested array', () => {
chai.expect(parse('a[][][]&a[][][]&a[][][]&a[][]&a[][]&a[]')).to.be.deep.equal({
expect('a[][][]&a[][][]&a[][][]&a[][]&a[][]&a[]', {
a: [[['', '', '']], ['', ''], ''],
})
})

it('array with index', () => {
chai.expect(parse('a[3]=3&a[5]=5&a[2]=2&a[4]=4&a[1]=1&a[0]=0&a[10]=10')).to.be.deep.equal({
expect('a[3]=3&a[5]=5&a[2]=2&a[4]=4&a[1]=1&a[0]=0&a[10]=10', {
a: ['0', '1', '2', '3', '4', '5', undefined, undefined, undefined, undefined, '10'],
})
})

it('object', () => {
chai.expect(parse('a[a]&a[b]&a[c]')).to.be.deep.equal({
expect('a[a]&a[b]&a[c]', {
a: { a: '', b: '', c: '' },
})
})

it('nested object', () => {
chai.expect(parse('a[a][a]&a[a][b]&a[a][c]')).to.be.deep.equal({
expect('a[a][a]&a[a][b]&a[a][c]', {
a: { a: { a: '', b: '', c: '' } },
})
})

it('mix array & object', () => {
chai.expect(parse('a[a][][a]&a[a][a]&a[a][][b]')).to.be.deep.equal({
a: { a: { 0: { a: '' }, a: '', 2:{ b: '' } } },
expect('a[a][][a][a]&a[a][a]&a[a][][b]', {
a: { a: { 0: { a: { a: '' } }, a: '', 2:{ b: '' } } },
})
expect('a[a][a][][b]&a[a][a][][]&a[a][a][][][c]', {
a: { a: { a: [ { b: '' }, [ '' ], [ { c: '' } ] ] } },
})
})

it('drop insecure key', () => {
chai.expect(parse('a[constructor][prototype][a]=1')).to.be.deep.equal({
expect('a[constructor][prototype][a]=1', {
a: undefined,
})
chai.expect(parse('a[toString]=1')).to.be.deep.equal({
a: undefined,
expect('a[a][toString]=1', {
a: { a: {} },
})
chai.expect(parse('a[__proto__]=1')).to.be.deep.equal({
a: undefined,
expect('a[][__proto__]=1', {
a: [{}],
})
})
})
24 changes: 18 additions & 6 deletions benchmark/index.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { run, bench, baseline } from 'mitata'
import { run, bench, group, baseline } from 'mitata'
import qs from 'qs'
import nqs from 'node:querystring'
import fq from 'fast-querystring'
import parse from '../index.mjs'

const payload = 'a&a&a&a&a&a&a&a&a&a&a&a&b[]&b[]&b[]&b[]&b[]&b[]&b[]&b[]&b[]&b[]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&c[a]&d[a][b][c][d][e][f]=1&d[a][b][c][d][e][f]=1&d[a][b][c][d][e][f]=1&d[a][b][c][d][e][f]=1&d[a][b][c][d][e][f]=1&d[a][b][c][d][e][f]=1'
group('basic', () => {
const payload = 'a&a&c&c&b[]&b[]&b[]'

baseline('faster-qs', () => parse(payload))

baseline('faster-qs', () => parse(payload))
bench('qs', () => qs.parse(payload))
bench('fast-querystring', () => fq.parse(payload))
bench('node:querystring', () => nqs.parse(payload))
})

bench('qs', () => qs.parse(payload))
bench('fast-querystring', () => fq.parse(payload))
bench('node:querystring', () => nqs.parse(payload))
group('deep object', () => {
const payload = 'a[a]&a[a][b]&a[a][b][c]&a[a][b][c][d]&a[a][b][c][d]&a[a][b][c][d]&a[b]&a[b][c]&a[b][c][d]&a[b][c]'

baseline('faster-qs', () => parse(payload))

bench('qs', () => qs.parse(payload))
bench('fast-querystring', () => fq.parse(payload))
bench('node:querystring', () => nqs.parse(payload))
})

await run()
55 changes: 55 additions & 0 deletions benchmark/node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,59 @@ group('Array', () => {
bench('Array.push.apply', () => { const a = []; a.push.apply(a, [1]) })
})

group('Array extend', () => {
bench('Array.push.apply', () => { const a = []; a.push.apply(a, [1]) })
bench('Array.push spread', () => { const a = []; a.push(...[1]) })
})

group('typecheck', () => {
bench('typeof', () => typeof 1 === 'number')
bench('isArray', () => Array.isArray(1))
})

group('recursive vs iterative', () => {
bench('recursive', () => {
function factorial(n) {
if (n === 0) return 1
else return n * factorial(n - 1)
}
factorial(100)
})
bench('iterative', () => {
function factorial(n) {
let result = 1
for (let i = 2; i <= n; i++) {
result *= i
}
return result
}
factorial(100)
})
})

group('reduce vs loop', () => {
bench('reduce', () => {
const arr = [1, 2, 3, 4, 5]
arr.reduce((a, b) => a + b)
})
bench('loop', () => {
const arr = [1, 2, 3, 4, 5]
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
})
})

group('loop keys vs entries', () => {
bench('loop keys', () => {
const obj = { a: 1, b: 2, c: 3 }
for (const k in obj) obj[k];
})
bench('entries', () => {
const obj = { a: 1, b: 2, c: 3 }
Object.entries(obj).forEach(([k, v]) => {})
})
})

await run()
138 changes: 84 additions & 54 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,93 @@ const isPosInt = (str) => {
}

const resolvePath = (o, path, v=null, d) => {
if (!path) {
if (!o) return v
o.push(v)
return o
}
if (d === 0) return { [path]: v }
const l = path.indexOf('[')
const r = path.indexOf(']')
if (l === -1 || r === -1 || l > r) return { [path]: v }
const k = path.slice(l + 1, r) || o?.length || 0
const next = path.slice(r + 1)
if (isPosInt(k)) {
let i = Number(k)
if (!o) o = []
else if (!Array.isArray(o)) {
if (!k && !i) i = Object.keys(o).length
return {
...o,
[i]: resolvePath(o[i], next, v, d - 1),
let next = path
let cur = o
let l, r, k
for (; d > 0 || !next; d--) {
l = next.indexOf('[')
r = next.indexOf(']')
if (l === -1 || r === -1 || l > r) return { [next]: v }
k = next.slice(l + 1, r) || cur?.length || 0
next = next.slice(r + 1)
if (isPosInt(k)) {
let i = Number(k)
if (!cur) cur = []
if (!o) o = cur
if (!Array.isArray(cur)) {
if (!k && !i) i = Object.keys(cur).length
cur = cur[i] = {}
continue
}
const extend = i - cur.length + 1
if (extend > 0) {
cur.push.apply(cur, Array(extend))
}
if (next === '[]' && typeof v !== 'string') {
cur[i] = v
return o
} else if (next) {
if (next.startsWith('[]')) {
cur = cur[k] ??= []
} else {
if (!cur[i]) cur[i] = {}
else if (Array.isArray(cur[i]))
cur[i] = { ...cur[i] }
cur = cur[i]
}
} else {
cur[i] = v
return o
}
} else if (!{}[k]) {
if (!cur) cur = {}
if (!o) o = cur
if (next) {
if (next.startsWith('[]')) {
cur = cur[k] ??= []
} else {
if (!cur[k]) cur[k] = {}
else if (Array.isArray(cur[k]))
cur[k] = { ...cur[k] }
cur = cur[k]
}
} else {
cur[k] = v
return o
}
}
const extend = i - o.length + 1
if (extend > 0) {
o = [...o, ...Array(extend)]
}
if (next === '[]' && typeof v !== 'string' && (!o[i] || Array.isArray(o[i]))) {
o[i] = [...o[i] ?? [], ...v]
} else {
o[i] = resolvePath(o[i], next, v, d - 1)
}
return o
} else if (!{}[k]) {
return {
...o,
[k]: resolvePath(o?.[k], next, v, d - 1),
break
}
} else {
return o
}
if (Array.isArray(cur)) {
if (next) cur.push({ [next]: v })
else cur.push(v)
} else if (typeof cur === 'object') {
if (next) cur[next] = v
}
return o
}

export default (str, depth=5) =>
Object.entries(parser(str))
.reduce((o, [k, v]) => {
const l = k.indexOf('[')
if (l > 0 && k.indexOf(']') > l) {
const key = k.slice(0, l)
const path = k.slice(l)
if (path === '[]') {
if (key in o) {
if (!Array.isArray(o[key]))
o[key] = [o[key]]
o[key] = [].concat(o[key], v)
} else o[key] = v
} else {
o[key] = resolvePath(o[key], path, v, depth)
}
} else o[k] = v
return o
}, {})
export default (str, depth=5) => {
const data = parser(str)
const o = {}
let v, l
for (const k in data) {
v = data[k]
l = k.indexOf('[')
if (l > 0 && k.indexOf(']') > l) {
const key = k.slice(0, l)
const path = k.slice(l)
if (path === '[]') {
if (key in o) {
if (!Array.isArray(o[key]))
o[key] = [o[key]]
o[key] = [].concat(o[key], v)
} else o[key] = v
} else {
o[key] = resolvePath(o[key], path, v, depth)
}
} else o[k] = v
}
return o
}

0 comments on commit 0e32630

Please sign in to comment.