@qezor/structkit 1.0.1 → 1.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.
- package/README.md +81 -17
- package/index.d.ts +103 -0
- package/index.js +4 -0
- package/index.mjs +38 -0
- package/lib/array.js +139 -0
- package/lib/deep.js +435 -0
- package/lib/insert.js +62 -0
- package/lib/object.js +44 -0
- package/lib/range.js +55 -0
- package/lib/shared.js +64 -0
- package/lib/string.js +195 -0
- package/package.json +4 -2
- package/test.js +116 -2
package/lib/string.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const { normalizeRange, normalizeCount, clampIndex } = require("./range.js")
|
|
4
|
+
|
|
5
|
+
function toText(value) {
|
|
6
|
+
if (value == null) return ""
|
|
7
|
+
return String(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sliceText(value, options = {}) {
|
|
11
|
+
const text = toText(value)
|
|
12
|
+
const range = normalizeRange(text.length, options)
|
|
13
|
+
return text.slice(range.start, range.end)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function takeText(value, count = 1) {
|
|
17
|
+
return sliceText(value, {
|
|
18
|
+
start: 0,
|
|
19
|
+
count: normalizeCount(count, 1),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function takeRightText(value, count = 1) {
|
|
24
|
+
const text = toText(value)
|
|
25
|
+
const amount = normalizeCount(count, 1)
|
|
26
|
+
return text.slice(Math.max(0, text.length - amount))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dropText(value, count = 1) {
|
|
30
|
+
const text = toText(value)
|
|
31
|
+
return text.slice(Math.min(text.length, normalizeCount(count, 1)))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dropRightText(value, count = 1) {
|
|
35
|
+
const text = toText(value)
|
|
36
|
+
return text.slice(0, Math.max(0, text.length - normalizeCount(count, 1)))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function spliceText(value, deleteCount = 0, insertion = "", options = {}) {
|
|
40
|
+
const text = toText(value)
|
|
41
|
+
const removeCount = normalizeCount(deleteCount, 0)
|
|
42
|
+
const next = insertion == null ? "" : String(insertion)
|
|
43
|
+
const at = clampIndex(text.length, options.at ?? options.index ?? options.from ?? options.start, text.length)
|
|
44
|
+
return `${text.slice(0, at)}${next}${text.slice(Math.min(text.length, at + removeCount))}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveTextMatchIndex(text, needle, range, greedy) {
|
|
48
|
+
if (!needle) return range.start
|
|
49
|
+
if (greedy) {
|
|
50
|
+
const index = text.lastIndexOf(needle, Math.max(range.start, range.end - needle.length))
|
|
51
|
+
return index >= range.start && index < range.end ? index : -1
|
|
52
|
+
}
|
|
53
|
+
const index = text.indexOf(needle, range.start)
|
|
54
|
+
return index >= range.start && index < range.end ? index : -1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function insertText(value, insertion, options = {}) {
|
|
58
|
+
const text = toText(value)
|
|
59
|
+
const range = normalizeRange(text.length, options)
|
|
60
|
+
let at
|
|
61
|
+
|
|
62
|
+
if (options.before != null || options.after != null) {
|
|
63
|
+
const needle = String(options.before ?? options.after)
|
|
64
|
+
const matchIndex = resolveTextMatchIndex(text, needle, range, options.greedy === true || options.last === true)
|
|
65
|
+
if (matchIndex === -1) return text
|
|
66
|
+
at = options.before != null ? matchIndex : matchIndex + needle.length
|
|
67
|
+
} else {
|
|
68
|
+
at = clampIndex(text.length, options.at ?? options.index ?? options.from ?? options.start, text.length)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return spliceText(text, 0, insertion, { at })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function replaceRangeText(value, replacement, options = {}) {
|
|
75
|
+
const text = toText(value)
|
|
76
|
+
const range = normalizeRange(text.length, options)
|
|
77
|
+
return spliceText(text, range.length, replacement, { at: range.start })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function truncate(value, options = {}) {
|
|
81
|
+
const text = toText(value)
|
|
82
|
+
const maxLength = normalizeCount(options.maxLength ?? options.length, text.length)
|
|
83
|
+
const omission = options.omission == null ? "..." : String(options.omission)
|
|
84
|
+
const position = options.position || "end"
|
|
85
|
+
|
|
86
|
+
if (text.length <= maxLength) return text
|
|
87
|
+
if (maxLength <= omission.length) return omission.slice(0, maxLength)
|
|
88
|
+
|
|
89
|
+
const remaining = maxLength - omission.length
|
|
90
|
+
|
|
91
|
+
if (position === "start") {
|
|
92
|
+
return `${omission}${text.slice(text.length - remaining)}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (position === "middle") {
|
|
96
|
+
const left = Math.ceil(remaining / 2)
|
|
97
|
+
const right = Math.floor(remaining / 2)
|
|
98
|
+
return `${text.slice(0, left)}${omission}${text.slice(text.length - right)}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `${text.slice(0, remaining)}${omission}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findSubstring(value, needle, options = {}) {
|
|
105
|
+
const text = toText(value)
|
|
106
|
+
const search = String(needle == null ? "" : needle)
|
|
107
|
+
const range = normalizeRange(text.length, options)
|
|
108
|
+
|
|
109
|
+
if (!search) return range.start
|
|
110
|
+
|
|
111
|
+
if (options.last === true) {
|
|
112
|
+
const index = text.lastIndexOf(search, Math.max(range.start, range.end - search.length))
|
|
113
|
+
return index >= range.start && index < range.end ? index : -1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const index = text.indexOf(search, range.start)
|
|
117
|
+
return index >= range.start && index < range.end ? index : -1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function countSubstrings(value, needle, options = {}) {
|
|
121
|
+
const text = toText(value)
|
|
122
|
+
const search = String(needle == null ? "" : needle)
|
|
123
|
+
const range = normalizeRange(text.length, options)
|
|
124
|
+
const allowOverlap = options.allowOverlap === true
|
|
125
|
+
|
|
126
|
+
if (!search) return 0
|
|
127
|
+
|
|
128
|
+
let count = 0
|
|
129
|
+
let cursor = range.start
|
|
130
|
+
|
|
131
|
+
while (cursor < range.end) {
|
|
132
|
+
const index = text.indexOf(search, cursor)
|
|
133
|
+
if (index === -1 || index >= range.end) break
|
|
134
|
+
count += 1
|
|
135
|
+
cursor = index + (allowOverlap ? 1 : search.length)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return count
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function between(value, left, right, options = {}) {
|
|
142
|
+
const text = toText(value)
|
|
143
|
+
const startNeedle = String(left == null ? "" : left)
|
|
144
|
+
const endNeedle = String(right == null ? "" : right)
|
|
145
|
+
const range = normalizeRange(text.length, options)
|
|
146
|
+
|
|
147
|
+
const leftIndex = startNeedle
|
|
148
|
+
? text.indexOf(startNeedle, range.start)
|
|
149
|
+
: range.start
|
|
150
|
+
|
|
151
|
+
if (leftIndex === -1 || leftIndex >= range.end) return ""
|
|
152
|
+
|
|
153
|
+
const start = startNeedle ? leftIndex + startNeedle.length : leftIndex
|
|
154
|
+
const rightIndex = endNeedle
|
|
155
|
+
? options.greedy === true
|
|
156
|
+
? text.lastIndexOf(endNeedle, Math.max(start, range.end - endNeedle.length))
|
|
157
|
+
: text.indexOf(endNeedle, start)
|
|
158
|
+
: range.end
|
|
159
|
+
|
|
160
|
+
if (rightIndex === -1 || rightIndex > range.end || rightIndex < start) return ""
|
|
161
|
+
if (options.includeDelimiters) {
|
|
162
|
+
return text.slice(startNeedle ? leftIndex : start, endNeedle ? rightIndex + endNeedle.length : rightIndex)
|
|
163
|
+
}
|
|
164
|
+
return text.slice(start, rightIndex)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function splitLines(value) {
|
|
168
|
+
const text = toText(value)
|
|
169
|
+
if (!text) return []
|
|
170
|
+
return text.split(/\r\n|\n|\r/)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function splitWords(value) {
|
|
174
|
+
const text = toText(value).trim()
|
|
175
|
+
if (!text) return []
|
|
176
|
+
return text.split(/\s+/u)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
toText,
|
|
181
|
+
sliceText,
|
|
182
|
+
takeText,
|
|
183
|
+
takeRightText,
|
|
184
|
+
dropText,
|
|
185
|
+
dropRightText,
|
|
186
|
+
spliceText,
|
|
187
|
+
insertText,
|
|
188
|
+
replaceRangeText,
|
|
189
|
+
truncate,
|
|
190
|
+
findSubstring,
|
|
191
|
+
countSubstrings,
|
|
192
|
+
between,
|
|
193
|
+
splitLines,
|
|
194
|
+
splitWords,
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qezor/structkit",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Iterative array
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Iterative string, array, object, and deep-structure manipulation utilities with precise range and depth controls.",
|
|
5
5
|
"files": [
|
|
6
6
|
"LICENSE",
|
|
7
7
|
"README.md",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
},
|
|
30
30
|
"keywords": [
|
|
31
31
|
"array",
|
|
32
|
+
"string",
|
|
32
33
|
"object",
|
|
34
|
+
"deep",
|
|
33
35
|
"utility",
|
|
34
36
|
"lodash",
|
|
35
37
|
"iterative",
|
package/test.js
CHANGED
|
@@ -4,7 +4,21 @@ const test = require("node:test")
|
|
|
4
4
|
const assert = require("node:assert/strict")
|
|
5
5
|
const structkit = require("./index.js")
|
|
6
6
|
|
|
7
|
-
test("
|
|
7
|
+
test("string helpers support range-limited manipulation and searching", () => {
|
|
8
|
+
assert.equal(structkit.sliceText("wholesale-pricing", { start: 0, end: 9 }), "wholesale")
|
|
9
|
+
assert.equal(structkit.takeRightText("pricing", 4), "cing")
|
|
10
|
+
assert.equal(structkit.spliceText("bulkbuy", 0, "-", { at: 4 }), "bulk-buy")
|
|
11
|
+
assert.equal(structkit.insertText("bulkbuy", "-", { after: "bulk" }), "bulk-buy")
|
|
12
|
+
assert.equal(structkit.replaceRangeText("groupbuy", "pool", { start: 0, end: 5 }), "poolbuy")
|
|
13
|
+
assert.equal(structkit.truncate("The power of bulk buying", { maxLength: 14 }), "The power o...")
|
|
14
|
+
assert.equal(structkit.findSubstring("stationary notebooks", "note"), 11)
|
|
15
|
+
assert.equal(structkit.countSubstrings("aaaa", "aa", { allowOverlap: true }), 3)
|
|
16
|
+
assert.equal(structkit.between("Qezor [CADAAD] queue", "[", "]"), "CADAAD")
|
|
17
|
+
assert.equal(structkit.between("a[b]c[d]e", "[", "]", { greedy: true }), "b]c[d")
|
|
18
|
+
assert.deepEqual(structkit.splitWords("bulk buying for all"), ["bulk", "buying", "for", "all"])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("array helpers cover grouping, ranges, and ordered manipulation", () => {
|
|
8
22
|
const buyers = [
|
|
9
23
|
{ id: "u1", role: "buyer", qty: 2 },
|
|
10
24
|
{ id: "u2", role: "buyer", qty: 5 },
|
|
@@ -13,9 +27,21 @@ test("array helpers cover grouping uniqueness and sums", () => {
|
|
|
13
27
|
]
|
|
14
28
|
|
|
15
29
|
assert.deepEqual(structkit.chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]])
|
|
30
|
+
assert.deepEqual(structkit.sliceRange([1, 2, 3, 4, 5], { start: 1, end: 4 }), [2, 3, 4])
|
|
31
|
+
assert.deepEqual(structkit.take([1, 2, 3], 2), [1, 2])
|
|
32
|
+
assert.deepEqual(structkit.dropRight([1, 2, 3, 4], 2), [1, 2])
|
|
16
33
|
assert.deepEqual(structkit.compact([0, 1, false, 2, "", 3]), [1, 2, 3])
|
|
17
34
|
assert.deepEqual(structkit.uniq([1, 1, 2, 3, 3]), [1, 2, 3])
|
|
18
35
|
assert.deepEqual(structkit.uniqBy(buyers, "id").map((item) => item.id), ["u1", "u2", "u3"])
|
|
36
|
+
assert.deepEqual(structkit.spliceAt([1, 2, 5], 2, 0, [3, 4]), [1, 2, 3, 4, 5])
|
|
37
|
+
assert.deepEqual(structkit.insertAt([1, 3], 1, 2), [1, 2, 3])
|
|
38
|
+
assert.deepEqual(structkit.removeAt([1, 2, 3, 4], 1, 2), [1, 4])
|
|
39
|
+
assert.deepEqual(structkit.replaceRange([1, 2, 3, 4], [8, 9], { start: 1, end: 3 }), [1, 8, 9, 4])
|
|
40
|
+
assert.deepEqual(structkit.move(["a", "b", "c", "d"], 1, 3), ["a", "c", "b", "d"])
|
|
41
|
+
assert.equal(structkit.findIndexFrom([5, 7, 9, 12], (value) => value > 8, { start: 1 }), 2)
|
|
42
|
+
assert.equal(structkit.findLastIndexFrom([5, 7, 9, 12], (value) => value > 6, { end: 3 }), 2)
|
|
43
|
+
assert.deepEqual(structkit.mapRange([1, 2, 3, 4], (value) => value * 10, { start: 1, end: 3 }), [1, 20, 30, 4])
|
|
44
|
+
assert.deepEqual(structkit.filterRange([1, 2, 3, 4, 5], (value) => value % 2 === 0, { start: 1, end: 4 }), [2, 4])
|
|
19
45
|
assert.deepEqual(structkit.groupBy(buyers, "role"), {
|
|
20
46
|
buyer: [buyers[0], buyers[1]],
|
|
21
47
|
seller: [buyers[2], buyers[3]],
|
|
@@ -33,13 +59,20 @@ test("path and object helpers work iteratively", () => {
|
|
|
33
59
|
structkit.set(state, "queue.regions[0].city", "Pune")
|
|
34
60
|
structkit.set(state, "queue.regions[0].active", true)
|
|
35
61
|
structkit.set(state, ["queue", "meta", "moq"], 500)
|
|
62
|
+
structkit.set(state, "queue.label", "GroupBuy")
|
|
63
|
+
structkit.update(state, "queue.meta.moq", (value) => value + 100)
|
|
64
|
+
structkit.insertAtPath(state, "queue.regions", { city: "Mumbai" }, { index: 1 })
|
|
65
|
+
structkit.insertAtPath(state, "queue.label", " ", { before: "Buy" })
|
|
36
66
|
|
|
37
67
|
assert.equal(structkit.get(state, "queue.regions[0].city"), "Pune")
|
|
68
|
+
assert.equal(structkit.get(state, "queue.regions[1].city"), "Mumbai")
|
|
69
|
+
assert.equal(structkit.get(state, "queue.label"), "Group Buy")
|
|
70
|
+
assert.equal(structkit.get(state, "queue.meta.moq"), 600)
|
|
38
71
|
assert.equal(structkit.has(state, "queue.meta.moq"), true)
|
|
39
72
|
assert.deepEqual(structkit.tokenizePath("queue.regions[0].city"), ["queue", "regions", 0, "city"])
|
|
40
73
|
assert.deepEqual(structkit.pick(state, ["queue.meta.moq", "queue.regions[0].city"]), {
|
|
41
74
|
queue: {
|
|
42
|
-
meta: { moq:
|
|
75
|
+
meta: { moq: 600 },
|
|
43
76
|
regions: [{ city: "Pune" }],
|
|
44
77
|
},
|
|
45
78
|
})
|
|
@@ -93,3 +126,84 @@ test("mapValues and defaults keep object helpers practical", () => {
|
|
|
93
126
|
{ a: 1, b: 6 }
|
|
94
127
|
)
|
|
95
128
|
})
|
|
129
|
+
|
|
130
|
+
test("deep helpers support precise search and manipulation without recursion", () => {
|
|
131
|
+
const value = {
|
|
132
|
+
queue: {
|
|
133
|
+
regions: [
|
|
134
|
+
{ city: "Pune", meta: { active: true } },
|
|
135
|
+
{ city: "Mumbai", meta: { active: false } },
|
|
136
|
+
],
|
|
137
|
+
stats: { joined: 12 },
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entries = [...structkit.iterateDeep(value, {
|
|
142
|
+
fromPath: "queue",
|
|
143
|
+
maxDepth: 2,
|
|
144
|
+
includeContainers: false,
|
|
145
|
+
})]
|
|
146
|
+
assert.equal(entries.some((entry) => entry.path === "queue.regions[0].city"), true)
|
|
147
|
+
assert.equal(entries.some((entry) => entry.path === "queue.regions[0].meta.active"), false)
|
|
148
|
+
|
|
149
|
+
const found = structkit.findDeep(value, { path: "queue.regions[1].city" })
|
|
150
|
+
assert.equal(found.value, "Mumbai")
|
|
151
|
+
assert.equal(found.depth, 3)
|
|
152
|
+
assert.equal(found.pathDepth, 4)
|
|
153
|
+
|
|
154
|
+
const scoped = structkit.findDeep(value, { key: "city", depth: 2 }, {
|
|
155
|
+
fromPath: "queue",
|
|
156
|
+
includeContainers: false,
|
|
157
|
+
})
|
|
158
|
+
assert.equal(scoped.value, "Pune")
|
|
159
|
+
assert.equal(scoped.pathDepth, 3)
|
|
160
|
+
|
|
161
|
+
const filtered = structkit.filterDeep(value, { key: "city" })
|
|
162
|
+
assert.equal(filtered.length, 2)
|
|
163
|
+
|
|
164
|
+
const paths = structkit.findPaths(value, { includes: "joined" })
|
|
165
|
+
assert.deepEqual(paths, ["queue.stats.joined"])
|
|
166
|
+
|
|
167
|
+
const deepest = structkit.findDeep(value, { key: "active" }, {
|
|
168
|
+
includeContainers: false,
|
|
169
|
+
greedy: "deepest",
|
|
170
|
+
})
|
|
171
|
+
assert.equal(deepest.path, "queue.regions[1].meta.active")
|
|
172
|
+
|
|
173
|
+
const updated = structkit.mapDeep(value, { key: "city" }, (city) => city.toUpperCase())
|
|
174
|
+
assert.deepEqual(updated.queue.regions.map((item) => item.city), ["PUNE", "MUMBAI"])
|
|
175
|
+
|
|
176
|
+
const inserted = structkit.insertDeep(value, { path: "queue.regions" }, {
|
|
177
|
+
city: "Nagpur",
|
|
178
|
+
meta: { active: true },
|
|
179
|
+
}, { index: 1 })
|
|
180
|
+
assert.equal(inserted.queue.regions[1].city, "Nagpur")
|
|
181
|
+
assert.equal(value.queue.regions.length, 2)
|
|
182
|
+
|
|
183
|
+
const removed = structkit.removeDeep(value, { path: "queue.regions[1].meta.active" })
|
|
184
|
+
assert.equal(structkit.has(removed, "queue.regions[1].meta.active"), false)
|
|
185
|
+
assert.equal(structkit.has(value, "queue.regions[1].meta.active"), true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test("deep helpers can handle long chains without stack overflow and stop on limits", () => {
|
|
189
|
+
const root = {}
|
|
190
|
+
let current = root
|
|
191
|
+
|
|
192
|
+
for (let index = 0; index < 1500; index += 1) {
|
|
193
|
+
current.next = { depth: index }
|
|
194
|
+
current = current.next
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const found = structkit.findDeep(root, { key: "depth", depth: 1500 }, {
|
|
198
|
+
includeContainers: false,
|
|
199
|
+
maxDepth: 1500,
|
|
200
|
+
})
|
|
201
|
+
assert.equal(found.value, 1499)
|
|
202
|
+
assert.equal(found.pathDepth, 1501)
|
|
203
|
+
|
|
204
|
+
assert.throws(() => {
|
|
205
|
+
[...structkit.iterateDeep(root, {
|
|
206
|
+
maxNodes: 100,
|
|
207
|
+
})]
|
|
208
|
+
}, /maxNodes/)
|
|
209
|
+
})
|