Skip to content

Commit 52f35ca

Browse files
authored
Merge pull request #40 from KnightNiwrem/master
feat: Add `slice`
2 parents e3d9ed4 + 2d61322 commit 52f35ca

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

src/format.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,49 @@ export class FormattedString
508508
plain(text: string) {
509509
return fmt`${this}${text}`;
510510
}
511+
512+
/**
513+
* Returns a deep copy of a portion of this FormattedString
514+
* @param start The start index (inclusive), defaults to 0
515+
* @param end The end index (exclusive), defaults to text length
516+
* @returns A new FormattedString containing the sliced text and properly adjusted entities
517+
*/
518+
slice(start?: number, end?: number): FormattedString {
519+
const textLength = this.rawText.length;
520+
521+
// Normalize start: negative values should be treated as 0
522+
const sliceStart = Math.max(0, start ?? 0);
523+
const sliceEnd = end ?? textLength;
524+
525+
// Get the sliced text
526+
const slicedText = this.rawText.slice(sliceStart, sliceEnd);
527+
528+
// Filter and adjust entities that intersect with the slice range
529+
const slicedEntities: MessageEntity[] = [];
530+
531+
for (const entity of this.rawEntities) {
532+
const entityStart = entity.offset;
533+
const entityEnd = entity.offset + entity.length;
534+
535+
// Check if entity intersects with slice range
536+
if (entityEnd > sliceStart && entityStart < sliceEnd) {
537+
// Calculate the intersection
538+
const intersectionStart = Math.max(entityStart, sliceStart);
539+
const intersectionEnd = Math.min(entityEnd, sliceEnd);
540+
541+
// Create new entity with adjusted offset and length
542+
const newEntity: MessageEntity = {
543+
...entity,
544+
offset: intersectionStart - sliceStart,
545+
length: intersectionEnd - intersectionStart,
546+
};
547+
548+
slicedEntities.push(newEntity);
549+
}
550+
}
551+
552+
return new FormattedString(slicedText, slicedEntities);
553+
}
511554
}
512555

513556
function buildFormatter<T extends Array<unknown> = never>(

test/format.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,3 +773,138 @@ Deno.test("FormattedString - Static join method", () => {
773773
assertEquals(combinedResult.rawEntities[1]?.offset, 28); // After "Start: TextWithEntities and "
774774
assertEquals(combinedResult.rawEntities[1]?.length, 7); // "Caption"
775775
});
776+
777+
Deno.test("FormattedString - Instance slice method", () => {
778+
// Test the example from the problem statement
779+
const originalText = "hello bold and italic world";
780+
const entities: MessageEntity[] = [
781+
{ type: "bold", offset: 6, length: 4 },
782+
{ type: "italic", offset: 15, length: 6 },
783+
];
784+
const original = new FormattedString(originalText, entities);
785+
786+
const sliced = original.slice(6, 20);
787+
788+
assertInstanceOf(sliced, FormattedString);
789+
assertEquals(sliced.rawText, "bold and itali");
790+
assertEquals(sliced.rawEntities.length, 2);
791+
792+
// Test bold entity adjustment
793+
assertEquals(sliced.rawEntities[0]?.type, "bold");
794+
assertEquals(sliced.rawEntities[0]?.offset, 0);
795+
assertEquals(sliced.rawEntities[0]?.length, 4);
796+
797+
// Test italic entity adjustment
798+
assertEquals(sliced.rawEntities[1]?.type, "italic");
799+
assertEquals(sliced.rawEntities[1]?.offset, 9);
800+
assertEquals(sliced.rawEntities[1]?.length, 5);
801+
});
802+
803+
Deno.test("FormattedString - slice method edge cases", () => {
804+
const text = "Hello World Test";
805+
const entities: MessageEntity[] = [
806+
{ type: "bold", offset: 0, length: 5 }, // "Hello"
807+
{ type: "italic", offset: 6, length: 5 }, // "World"
808+
{ type: "code", offset: 12, length: 4 }, // "Test"
809+
];
810+
const original = new FormattedString(text, entities);
811+
812+
// Test slice without parameters (should return full copy)
813+
const fullSlice = original.slice();
814+
assertInstanceOf(fullSlice, FormattedString);
815+
assertEquals(fullSlice.rawText, text);
816+
assertEquals(fullSlice.rawEntities.length, 3);
817+
assertEquals(fullSlice.rawEntities[0]?.type, "bold");
818+
assertEquals(fullSlice.rawEntities[1]?.type, "italic");
819+
assertEquals(fullSlice.rawEntities[2]?.type, "code");
820+
821+
// Test slice with only start parameter
822+
const partialSlice = original.slice(6);
823+
assertEquals(partialSlice.rawText, "World Test");
824+
assertEquals(partialSlice.rawEntities.length, 2);
825+
assertEquals(partialSlice.rawEntities[0]?.type, "italic");
826+
assertEquals(partialSlice.rawEntities[0]?.offset, 0);
827+
assertEquals(partialSlice.rawEntities[0]?.length, 5);
828+
829+
// Test slice that partially overlaps entities
830+
const overlappingSlice = original.slice(3, 9);
831+
assertEquals(overlappingSlice.rawText, "lo Wor");
832+
assertEquals(overlappingSlice.rawEntities.length, 2);
833+
834+
// Bold entity should be partially included
835+
assertEquals(overlappingSlice.rawEntities[0]?.type, "bold");
836+
assertEquals(overlappingSlice.rawEntities[0]?.offset, 0);
837+
assertEquals(overlappingSlice.rawEntities[0]?.length, 2); // "lo"
838+
839+
// Italic entity should be partially included
840+
assertEquals(overlappingSlice.rawEntities[1]?.type, "italic");
841+
assertEquals(overlappingSlice.rawEntities[1]?.offset, 3);
842+
assertEquals(overlappingSlice.rawEntities[1]?.length, 3); // "Wor"
843+
844+
// Test slice that excludes all entities
845+
const noEntitiesSlice = original.slice(1, 2);
846+
assertEquals(noEntitiesSlice.rawText, "e");
847+
assertEquals(noEntitiesSlice.rawEntities.length, 1);
848+
assertEquals(noEntitiesSlice.rawEntities[0]?.type, "bold");
849+
assertEquals(noEntitiesSlice.rawEntities[0]?.offset, 0);
850+
assertEquals(noEntitiesSlice.rawEntities[0]?.length, 1);
851+
});
852+
853+
Deno.test("FormattedString - slice method with empty string", () => {
854+
const empty = new FormattedString("", []);
855+
const sliced = empty.slice(0, 0);
856+
857+
assertInstanceOf(sliced, FormattedString);
858+
assertEquals(sliced.rawText, "");
859+
assertEquals(sliced.rawEntities.length, 0);
860+
});
861+
862+
Deno.test("FormattedString - slice method boundary conditions", () => {
863+
const text = "abcdef";
864+
const entities: MessageEntity[] = [
865+
{ type: "bold", offset: 1, length: 4 }, // "bcde"
866+
];
867+
const original = new FormattedString(text, entities);
868+
869+
// Test slice at entity boundaries
870+
const exactSlice = original.slice(1, 5);
871+
assertEquals(exactSlice.rawText, "bcde");
872+
assertEquals(exactSlice.rawEntities.length, 1);
873+
assertEquals(exactSlice.rawEntities[0]?.type, "bold");
874+
assertEquals(exactSlice.rawEntities[0]?.offset, 0);
875+
assertEquals(exactSlice.rawEntities[0]?.length, 4);
876+
877+
// Test slice that goes beyond text length
878+
const beyondSlice = original.slice(0, 100);
879+
assertEquals(beyondSlice.rawText, text);
880+
assertEquals(beyondSlice.rawEntities.length, 1);
881+
882+
// Test slice with negative start (should be treated as 0)
883+
const negativeStart = original.slice(-5, 3);
884+
assertEquals(negativeStart.rawText, "abc");
885+
assertEquals(negativeStart.rawEntities.length, 1);
886+
assertEquals(negativeStart.rawEntities[0]?.type, "bold");
887+
assertEquals(negativeStart.rawEntities[0]?.offset, 1);
888+
assertEquals(negativeStart.rawEntities[0]?.length, 2); // "bc"
889+
});
890+
891+
Deno.test("FormattedString - slice method creates deep copy", () => {
892+
const original = new FormattedString("hello world", [
893+
{ type: "bold", offset: 0, length: 5 },
894+
]);
895+
896+
const sliced = original.slice(0, 7);
897+
898+
// Verify it's a different object
899+
assertInstanceOf(sliced, FormattedString);
900+
assertEquals(sliced !== original, true);
901+
assertEquals(sliced.rawEntities !== original.rawEntities, true);
902+
assertEquals(sliced.rawEntities[0] !== original.rawEntities[0], true);
903+
904+
// Verify the sliced result has the correct content
905+
assertEquals(sliced.rawText, "hello w");
906+
assertEquals(sliced.rawEntities.length, 1);
907+
assertEquals(sliced.rawEntities[0]?.type, "bold");
908+
assertEquals(sliced.rawEntities[0]?.offset, 0);
909+
assertEquals(sliced.rawEntities[0]?.length, 5);
910+
});

0 commit comments

Comments
 (0)