@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/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.1",
4
- "description": "Iterative array and object utilities for high-use data shaping without callback hell or recursion-heavy internals.",
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("array helpers cover grouping uniqueness and sums", () => {
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: 500 },
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
+ })