Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/four-cooks-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Comment on lines +1 to +2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a changeset if we want to release the change.

63 changes: 63 additions & 0 deletions packages/sdk/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,66 @@ describe("ReplexicaEngine", () => {
});
});
});


describe("LingoDotDevEngine - countWordsInRecord", () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });

it("should return 0 for falsy values and empty inputs", () => {
expect((engine as any).countWordsInRecord(null)).toBe(0);
expect((engine as any).countWordsInRecord(undefined)).toBe(0);
expect((engine as any).countWordsInRecord("")).toBe(0);
expect((engine as any).countWordsInRecord({})).toBe(0);
expect((engine as any).countWordsInRecord([])).toBe(0);
});

it("should count words in a simple string", () => {
expect((engine as any).countWordsInRecord("Hello world")).toBe(2);
expect((engine as any).countWordsInRecord(" one two three ")).toBe(3);
expect((engine as any).countWordsInRecord("\tNew\nlines and\ttabs\r\n")).toBe(4);
});

it("should count words in an array of strings", () => {
// "a" -> 1, "b c" -> 2, "d" -> 1
expect((engine as any).countWordsInRecord(["a", "b c", "d"])).toBe(1 + 2 + 1);
});

it("should count words in a nested array", () => {
// Here, the function is fully recursive:
// "a" → 1, "b c" → 2, and nested ["d e"] → "d e" → 2 words.
// Total = 1 + 2 + 2 = 5 words.
expect((engine as any).countWordsInRecord(["a", "b c", ["d e"]])).toBe(5);
});

it("should count words in a nested object", () => {
// Object { a: "hello", b: { c: "c d" } }:
// "hello" → 1, and "c d" → 2 words.
// Total = 1 + 2 = 3.
expect((engine as any).countWordsInRecord({ a: "hello", b: { c: "c d" } })).toBe(3);
});
it("should perform efficiently on large payloads", () => {
// Generate a large payload with many key-value pairs (in lakhs)
const largePayload: Record<string, string> = {};
const sampleText = "This is a sample text for benchmarking the countWordsInRecord function";
// Count words in sampleText:
// ["This", "is", "a", "sample", "text", "for", "benchmarking", "the", "countWordsInRecord", "function"]
// That is 10 words.
const repetitions = 100_000; // 1 lakh
for (let i = 0; i < repetitions; i++) {
largePayload[`key_${i}`] = sampleText;
}
const expectedTotalWords = repetitions * 10;

const startTime = performance.now();
const result = (engine as any).countWordsInRecord(largePayload);
const endTime = performance.now();
const elapsed = endTime - startTime;

console.log(`Large payload processed in ${elapsed.toFixed(2)} ms`);

expect(result).toBe(expectedTotalWords);
// Optionally, assert that the function finishes within a reasonable time threshold:
expect(elapsed).toBeLessThan(5000); // e.g., under 5 seconds for 1 lakh entries
});
});

41 changes: 33 additions & 8 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,43 @@ export class LingoDotDevEngine {
* @param payload - The payload to count words in
* @returns The total number of words
*/
// private countWordsInRecord(payload: any | Record<string, any> | Array<any>): number {
// if (Array.isArray(payload)) {
// return payload.reduce((acc, item) => acc + this.countWordsInRecord(item), 0);
// } else if (typeof payload === "object" && payload !== null) {
// return Object.values(payload).reduce((acc: number, item) => acc + this.countWordsInRecord(item), 0);
// } else if (typeof payload === "string") {
// return payload.trim().split(/\s+/).filter(Boolean).length;
// } else {
// return 0;
// }
// }

private countWordsInRecord(payload: any | Record<string, any> | Array<any>): number {
if (Array.isArray(payload)) {
return payload.reduce((acc, item) => acc + this.countWordsInRecord(item), 0);
} else if (typeof payload === "object" && payload !== null) {
return Object.values(payload).reduce((acc: number, item) => acc + this.countWordsInRecord(item), 0);
} else if (typeof payload === "string") {
return payload.trim().split(/\s+/).filter(Boolean).length;
} else {
return 0;
const stack: any[] = [payload];
let totalWordCount = 0;

while (stack.length > 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this an optimization? If I am reading this right, the code is executed the same number of times as before. This is a micro-optimization at best (if while is faster than recursion), however I find this new code harder to read than before.

I'd like a second pair of eyes on this, @maxprilutskiy @7hacker please have a look.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mathio I can come up with a better optimization for this making it more readable.Should I make a new pr or add the changes in this branch?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I raised my concerns in the issue. Lets continue the conversation there.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New Optimizations
Screenshot 2025-03-27 084005
->Improved execution time for large and deeply nested payloads.
->Lower memory consumption by eliminating redundant allocations.
->Removed Recursion because it might cause stack overflow.

const current = stack.pop();

if (current === null || current === undefined) {
continue;
}

if (Array.isArray(current)) {
stack.push(...current);
} else if (typeof current === 'object') {
stack.push(...Object.values(current));
} else if (typeof current === 'string') {
totalWordCount += current.trim().split(/\s+/).filter(Boolean).length;
}
}

return totalWordCount;
}



/**
* Localize a typical JavaScript object
* @param obj - The object to be localized (strings will be extracted and translated)
Expand Down