Skip to content

Instantly share code, notes, and snippets.

@unicomp21
Created April 24, 2023 12:11
Show Gist options
  • Save unicomp21/3e67673dbd9c86f0b89ac2f1c63b2bb7 to your computer and use it in GitHub Desktop.
Save unicomp21/3e67673dbd9c86f0b89ac2f1c63b2bb7 to your computer and use it in GitHub Desktop.

Revisions

  1. unicomp21 created this gist Apr 24, 2023.
    25 changes: 25 additions & 0 deletions common.impl.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    import {HexLengthDelimitedInt} from "./hexLengthDelimitedInt.impl";

    function processSerializedString(input: string, offset: number): { length: number, value: string, nextOffset: number } {
    const lengthResult = HexLengthDelimitedInt.deserialize(input, offset);
    offset = lengthResult.nextOffset;
    const value = input.slice(offset, offset + lengthResult.number);
    offset += lengthResult.number;
    return {length: lengthResult.number, value, nextOffset: offset};
    }

    export function serializeTagValue(tag: string, value: string): string {
    const serializedTag = HexLengthDelimitedInt.serialize(tag.length) + tag;
    const serializedValue = HexLengthDelimitedInt.serialize(value.length) + value;
    return serializedTag + serializedValue + '\n';
    }

    export function deserializeTagValue(input: string, offset: number): { tag: string, value: string, nextOffset: number } {
    const tagResult = processSerializedString(input, offset);
    offset = tagResult.nextOffset;

    const valueResult = processSerializedString(input, offset);
    offset = valueResult.nextOffset;

    return {tag: tagResult.value, value: valueResult.value, nextOffset: offset};
    }
    66 changes: 66 additions & 0 deletions hexLengthDelimitedInt.impl.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,66 @@
    function containsOnlyDecimalDigits(input: string): boolean {
    for (const char of input) {
    if (char < '0' || char > '9') {
    return false;
    }
    }
    return true;
    }

    export interface DeserializationResult {
    number: number;
    nextOffset: number;
    }

    export class HexLengthDelimitedInt {
    static deserialize(input: string, offset = 0): DeserializationResult {
    if (input[offset] !== '(') {
    throw new Error("Invalid input format");
    }

    let nextOffset = -1;
    for (let i = offset + 1; i < input.length; i++) {
    if (input[i] === ')') {
    nextOffset = i;
    break;
    }
    }

    if (nextOffset === -1) {
    throw new Error("Invalid input format");
    }

    const length = parseInt(input.slice(offset + 1, offset + 2), 16);
    if (isNaN(length) || length < 0 || length > 15) {
    throw new Error("Invalid input length");
    }

    const dec = input.slice(offset + 2, nextOffset);
    if (!containsOnlyDecimalDigits(dec)) {
    throw new Error("Invalid decimal digits in length-delimited integer");
    }

    const number = parseInt(dec, 10);

    if (!Number.isInteger(number)) {
    throw new Error("Deserialized number is not an integer");
    }

    return {number, nextOffset: nextOffset + 1};
    }

    static serialize(number: number): string {
    if (!Number.isInteger(number)) {
    throw new Error("Input number is not an integer");
    }

    const dec = number.toString(10);
    const length = dec.length;

    if (length > 15) {
    throw new Error("Number too large to serialize");
    }

    return '(' + length.toString(16) + dec + ')';
    }
    }
    50 changes: 50 additions & 0 deletions hexLengthDelimitedInt.test.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,50 @@
    import {HexLengthDelimitedInt} from "./hexLengthDelimitedInt.impl";

    describe('HexLengthDelimitedInt', () => {
    test('should serialize and deserialize an integer correctly', () => {
    const number = 12345;
    const serialized = HexLengthDelimitedInt.serialize(number);
    expect(serialized).toBe('(512345)');

    const {number: deserialized, nextOffset} = HexLengthDelimitedInt.deserialize(serialized);
    expect(deserialized).toBe(number);
    expect(nextOffset).toBe(8);
    });

    test('should throw an error when serializing a non-integer', () => {
    const number = 123.45;
    expect(() => HexLengthDelimitedInt.serialize(number)).toThrow('Input number is not an integer');
    });

    test('should throw an error when deserializing a non-integer', () => {
    const input = '(51234.5)';
    expect(() => HexLengthDelimitedInt.deserialize(input)).toThrow('Invalid decimal digits in length-delimited integer');
    });

    test('should throw an error when deserializing an invalid input format', () => {
    const input1 = '512345)';
    expect(() => HexLengthDelimitedInt.deserialize(input1)).toThrow('Invalid input format');

    const input2 = '(512345';
    expect(() => HexLengthDelimitedInt.deserialize(input2)).toThrow('Invalid input format');
    });

    test('should throw an error when deserializing an input with invalid length', () => {
    const input = '(g123456123)';
    expect(() => HexLengthDelimitedInt.deserialize(input)).toThrow('Invalid input length');
    });

    test('should throw an error when serializing a number too large to serialize', () => {
    const number = 0xFFFFFFFFFFFFFFFF;
    expect(() => HexLengthDelimitedInt.serialize(number)).toThrow('Number too large to serialize');
    });

    test('should deserialize multiple numbers from the same input string', () => {
    const input = "(512345)(6123456)";
    const {number: deserialized1, nextOffset} = HexLengthDelimitedInt.deserialize(input);
    expect(deserialized1).toBe(12345);

    const {number: deserialized2} = HexLengthDelimitedInt.deserialize(input, nextOffset);
    expect(deserialized2).toBe(123456);
    });
    });
    84 changes: 84 additions & 0 deletions tagValueArray.impl.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,84 @@
    import {deserializeTagValue, serializeTagValue} from "./common.impl";
    import {HexLengthDelimitedInt} from "./hexLengthDelimitedInt.impl";

    export class TagValueArray {
    public static serialize(array: [string, string][]): string {
    let result = '';

    result += HexLengthDelimitedInt.serialize(array.length) + '\n';

    for (let i = 0; i < array.length; i++) {
    const [tag, value] = array[i];
    result += serializeTagValue(tag, value);
    }

    return result;
    }

    static serializeNoLength(tagValueArray: Array<[string, string]>): string {
    let message = '';

    for (const [tag, value] of tagValueArray) {
    message += serializeTagValue(tag, value);
    }

    return message;
    }

    public static serializeLength(array: [string, string][]): number {
    let length = 0;

    length += HexLengthDelimitedInt.serialize(array.length).length + 1; // Add 1 for the newline character

    for (let i = 0; i < array.length; i++) {
    const [tag, value] = array[i];
    length += serializeTagValue(tag, value).length;
    }

    return length;
    }

    private static processTagValuePairs(input: string, count: number, offset: number): Array<[string, string]> {
    const tagValueArray: Array<[string, string]> = [];

    for (let i = 0; i < count; i++) {
    const { tag, value, nextOffset } = deserializeTagValue(input, offset);
    offset = nextOffset;

    tagValueArray.push([tag, value]);

    if (i < count - 1 && input[offset] !== '\n') {
    throw new Error("Invalid input format: missing newline character after tag-value pair");
    }
    offset++;
    }

    return tagValueArray;
    }

    static deserialize(input: string): { array: Array<[string, string]>, nextOffset: number } {
    const countResult = HexLengthDelimitedInt.deserialize(input);
    const count = countResult.number;
    let offset = countResult.nextOffset;

    if (input[offset] !== '\n') {
    throw new Error("Invalid input format: missing newline character after count");
    }
    offset++;

    const array = TagValueArray.processTagValuePairs(input, count, offset);
    return { array, nextOffset: offset };
    }

    static deserializeNoLength(input: string, count: number): Array<[string, string]> {
    return TagValueArray.processTagValuePairs(input, count, 0);
    }

    static isEmpty(tagValueArray: Array<[string, string]>): boolean {
    return tagValueArray.length === 0;
    }

    static hasTag(tagValueArray: Array<[string, string]>, tag: string): boolean {
    return tagValueArray.some(([t, _]) => t === tag);
    }
    }
    66 changes: 66 additions & 0 deletions tagValueArray.test.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,66 @@
    import {TagValueArray} from "./tagValueArray.impl";
    import {HexLengthDelimitedInt} from "./hexLengthDelimitedInt.impl";

    describe('TagValueArray', () => {
    const testCases = [
    {
    name: 'Empty array',
    input: [] as [string, string][],
    },
    {
    name: 'Single tag-value pair',
    input: [['tag1', 'value1']] as [string, string][],
    },
    {
    name: 'Multiple tag-value pairs',
    input: [
    ['tag1', 'value1'],
    ['tag2', 'value2'],
    ['tag3', 'value3'],
    ] as [string, string][],
    },
    {
    name: 'Long tag and value',
    input: [['tagWithLongName', 'valueWithLongContent']] as [string, string][],
    },
    ];

    testCases.forEach(testCase => {
    test(`Serialize and deserialize: ${testCase.name}`, () => {
    const serialized = TagValueArray.serialize(testCase.input);
    const deserialized = TagValueArray.deserialize(serialized);

    expect(deserialized.array).toEqual(testCase.input);
    });
    });

    test('Serialize and deserialize: Empty tag or value', () => {
    const input: [string, string][] = [['tag1', ''], ['', 'value2']];
    const serialized = TagValueArray.serialize(input);
    const deserialized = TagValueArray.deserialize(serialized);
    expect(deserialized.array).toEqual(input);
    });

    test('Serialize and deserialize: Tag or value containing newline characters', () => {
    const input: [string, string][] = [['tag\n1', 'value\n1'], ['tag2', 'value\n2']];
    const serialized = TagValueArray.serialize(input);
    const deserialized = TagValueArray.deserialize(serialized);

    expect(deserialized.array).toEqual(input);
    });

    test('Serialize and deserialize: Tag or value containing special characters', () => {
    const input: [string, string][] = [['täg1', 'vàlué1'], ['t🚀g2', 'v🌍luè2']];
    const serialized = TagValueArray.serialize(input);
    const deserialized = TagValueArray.deserialize(serialized);

    expect(deserialized.array).toEqual(input);
    });

    test('SerializeLength', () => {
    const input: [string, string][] = [['tag1', 'value1'], ['tag2', 'value2'], ['tag3', 'value3']];
    const length = TagValueArray.serializeLength(input);
    const serialized = TagValueArray.serialize(input);
    expect(length).toEqual(serialized.length);
    });
    });
    49 changes: 49 additions & 0 deletions tagValueMessage.impl.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    import {TagValueArray} from "./tagValueArray.impl";

    export class TagValueMessage {
    private tagValueMap: Map<string, string>;

    constructor() {
    this.tagValueMap = new Map<string, string>();
    }

    set(tag: string, value: string): void {
    this.tagValueMap.set(tag, value);
    }

    get(tag: string): string | undefined {
    return this.tagValueMap.get(tag);
    }

    has(tag: string): boolean {
    return this.tagValueMap.has(tag);
    }

    delete(tag: string): boolean {
    return this.tagValueMap.delete(tag);
    }

    clear(): void {
    this.tagValueMap.clear();
    }

    serialize(): string {
    const tagValueArray: Array<[string, string]> = [];
    for (const [tag, value] of this.tagValueMap.entries()) {
    tagValueArray.push([tag, value]);
    }

    return TagValueArray.serialize(tagValueArray);
    }

    deserialize(input: string): number {
    const { array, nextOffset } = TagValueArray.deserialize(input);
    this.tagValueMap.clear();

    for (const [tag, value] of array) {
    this.set(tag, value);
    }

    return nextOffset;
    }
    }
    53 changes: 53 additions & 0 deletions tagValueMessage.test.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    import {TagValueMessage} from "./tagValueMessage.impl";

    describe('TagValueMessage', () => {
    let message: TagValueMessage;

    beforeEach(() => {
    message = new TagValueMessage();
    });

    test('set, get, has, and delete methods', () => {
    expect(message.has('tag1')).toBe(false);
    expect(message.get('tag1')).toBeUndefined();

    message.set('tag1', 'value1');
    expect(message.has('tag1')).toBe(true);
    expect(message.get('tag1')).toBe('value1');

    message.set('tag2', 'value2');
    expect(message.has('tag2')).toBe(true);
    expect(message.get('tag2')).toBe('value2');

    expect(message.delete('tag1')).toBe(true);
    expect(message.has('tag1')).toBe(false);
    expect(message.get('tag1')).toBeUndefined();

    expect(message.delete('nonexistent')).toBe(false);
    });

    test('clear method', () => {
    message.set('tag1', 'value1');
    message.set('tag2', 'value2');

    message.clear();
    expect(message.has('tag1')).toBe(false);
    expect(message.get('tag1')).toBeUndefined();
    expect(message.has('tag2')).toBe(false);
    expect(message.get('tag2')).toBeUndefined();
    });

    test('serialize and deserialize methods', () => {
    message.set('tag1', 'value1');
    message.set('tag2', 'value2');

    const serialized = message.serialize();
    const deserializedMessage = new TagValueMessage();
    deserializedMessage.deserialize(serialized);

    expect(deserializedMessage.has('tag1')).toBe(true);
    expect(deserializedMessage.get('tag1')).toBe('value1');
    expect(deserializedMessage.has('tag2')).toBe(true);
    expect(deserializedMessage.get('tag2')).toBe('value2');
    });
    });