-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from fossapps/add_number_compressor
Add number compressor
- Loading branch information
Showing
10 changed files
with
213 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
# tools | ||
.vscode/ | ||
.idea/ | ||
.idea/* | ||
|
||
!.idea/runConfigurations/* | ||
!.idea/runConfigurations/ | ||
# builds | ||
lib/ | ||
lib.es2015/ | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import {ValueType} from "../Searilie"; | ||
import {TinyNumberCompressor} from "./TinyNumberCompressor"; | ||
|
||
describe("TinyNumberCompressor", () => { | ||
it("should be defined", () => { | ||
expect(TinyNumberCompressor).toBeDefined(); | ||
}); | ||
|
||
it("[important] should return an identifier", () => { | ||
// DO NOT CHANGE THIS TEST CASE once we change the identifier, all old values can't be deserialized | ||
const adapter = new TinyNumberCompressor(jest.fn()); | ||
expect(adapter.getIdentifier()).toBe("C"); | ||
}); | ||
|
||
describe("constructor", () => { | ||
it("should accept keyLengthFactoryFunction", () => { | ||
expect(() => new TinyNumberCompressor(jest.fn())).not.toThrow(); | ||
}); | ||
}); | ||
|
||
describe("validation", () => { | ||
it("should throw error for string values", () => { | ||
const mockFn = jest.fn(); | ||
const tinyNumberCompressor = new TinyNumberCompressor(mockFn); | ||
expect(() => tinyNumberCompressor.serialize([{a: "something"}])).toThrow(); | ||
expect(() => tinyNumberCompressor.serialize([{a: "s"}])).toThrow(); | ||
}); | ||
|
||
it("should throw error if number doesn't fit in given number of spaces", () => { | ||
const mockFn = jest.fn(() => 1); | ||
const tinyNumberCompressor = new TinyNumberCompressor(mockFn); | ||
expect(() => tinyNumberCompressor.serialize([{a: 36}])).toThrow(); | ||
expect(() => tinyNumberCompressor.serialize([{a: 35}])).not.toThrow(); | ||
expect(mockFn).toHaveBeenCalled(); | ||
// if mock returns 2 it can store upto 1295 | ||
mockFn.mockImplementation(() => 2); | ||
expect(() => tinyNumberCompressor.serialize([{a: 36}])).not.toThrow(); | ||
expect(() => tinyNumberCompressor.serialize([{a: 1295}])).not.toThrow(); | ||
expect(() => tinyNumberCompressor.serialize([{a: 1296}])).toThrow(); | ||
}); | ||
}); | ||
describe("TinyNumberCompressor encoding", () => { | ||
it("should be able to encode data", () => { | ||
const mockFn = jest.fn(() => 1); | ||
const tinyNumberCompressor = new TinyNumberCompressor(mockFn); | ||
expect(tinyNumberCompressor.serialize([])).toBe(""); | ||
expect(tinyNumberCompressor.serialize([{a: 29, b: 18, c: 23, d: 34}])).toBe("tiny"); | ||
mockFn.mockImplementation(() => 2); | ||
expect(tinyNumberCompressor.serialize([{a: 29, b: 18, c: 23, d: 34}])).toBe("0t0i0n0y"); | ||
mockFn.mockImplementation((...args: any[]) => args[0] === "a" ? 2 : 1); | ||
expect(tinyNumberCompressor.serialize([{a: 29, b: 18, c: 23, d: 34}])).toBe("0tiny"); | ||
expect(tinyNumberCompressor.serialize([{a: 630, b: 16, c: 17, d: 5}])).toBe("high5"); | ||
}); | ||
}); | ||
describe("deserialization", () => { | ||
it("should throw error if length is mismatched", () => { | ||
const tinyNumberCompressor = new TinyNumberCompressor(jest.fn(() => 2)); | ||
// we have 2 spaces, if we pass 1 or 3 character, it should be invalid | ||
expect(() => tinyNumberCompressor.deserialize("s", {a: ValueType.Number})).toThrow("invalid data"); | ||
expect(() => tinyNumberCompressor.deserialize("dog", {a: ValueType.Number})).toThrow("invalid data"); | ||
expect(() => tinyNumberCompressor.deserialize("dog", {a: ValueType.Number, b: ValueType.Number})).toThrow("invalid data"); | ||
expect(() => tinyNumberCompressor.deserialize("dogs", {a: ValueType.Number, b: ValueType.Number})).not.toThrow("invalid data"); | ||
}); | ||
it("should deserialize correctly", () => { | ||
const tinyNumberCompressor = new TinyNumberCompressor(jest.fn(() => 2)); | ||
// we have 2 spaces, if we pass 1 or 3 character, it should be invalid | ||
expect(tinyNumberCompressor.deserialize("dogs", {a: ValueType.Number, b: ValueType.Number})).toStrictEqual([{a: 492, b: 604}]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import {IAdapter, IObject, ISchema, TIdentifier} from "../Searilie"; | ||
import {chunkText, ExtractText} from "../utils/ChunkText"; | ||
import {Validator} from "../validation/Validator"; | ||
|
||
type KeyLengthFactory = (key: string) => number; | ||
|
||
export class TinyNumberCompressor implements IAdapter { | ||
constructor(private keyLengthFactory: KeyLengthFactory) { | ||
this.encodeObject = this.encodeObject.bind(this); | ||
} | ||
|
||
public static getMaxNumberForNumberOfCharacters(numOfChars: number): number { | ||
return (36 ** numOfChars) - 1; | ||
} | ||
|
||
public deserialize(text: string, schema: ISchema): IObject[] { | ||
const charLengthForSchema = this.getCharLengthForSchema(schema); | ||
if (text.length % charLengthForSchema !== 0) { | ||
throw new Error("invalid data"); | ||
} | ||
// chunk items | ||
const chunks = chunkText(text, charLengthForSchema); | ||
// each chunk needs to be deserialized and returned | ||
// this means it's valid | ||
return chunks.map((x) => this.decodeChunk(x, schema)); | ||
} | ||
|
||
public getIdentifier(): TIdentifier { | ||
return "C"; | ||
} | ||
|
||
public serialize(object: IObject[]): string { | ||
// validate first, | ||
if (!this.isValid(object)) { | ||
throw new Error("invalid data"); | ||
} | ||
return object.map(this.encodeObject).join(""); | ||
} | ||
|
||
private getCharLengthForSchema(schema: ISchema): number { | ||
return Object.keys(schema).map((x) => this.keyLengthFactory(x)).reduce((a, b) => a + b, 0); | ||
} | ||
|
||
private encodeObject(object: IObject): string { | ||
return Object.keys(object).sort().map((x) => { | ||
const length = this.keyLengthFactory(x); | ||
return (object[x] as number).toString(36).padStart(length, "0"); | ||
}).join(""); | ||
} | ||
|
||
private isValid(object: IObject[]): boolean { | ||
return Validator.validateArray(object, (value, key) => { | ||
if (typeof value !== "number") { | ||
return false; | ||
} | ||
const spaceAllocatedForKey = this.keyLengthFactory(key); | ||
const maxValueForKeySize = TinyNumberCompressor.getMaxNumberForNumberOfCharacters(spaceAllocatedForKey); | ||
return value <= maxValueForKeySize; | ||
}); | ||
} | ||
|
||
private decodeChunk(chunk: string, schema: ISchema): IObject { | ||
const object: IObject = {}; | ||
const textExtractor = new ExtractText(chunk); | ||
const keys = Object.keys(schema).sort(); | ||
for (const key of keys) { | ||
const length = this.keyLengthFactory(key); | ||
object[key] = parseInt(textExtractor.extract(length), 36); | ||
} | ||
return object; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import {chunkText, ExtractText} from "./ChunkText"; | ||
|
||
describe("ChunkText", () => { | ||
it("should be able to chunk text into parts", () => { | ||
expect(chunkText("abcdef", 3)).toStrictEqual(["abc", "def"]); | ||
expect(chunkText("abcde", 3)).toStrictEqual(["abc", "de"]); | ||
}); | ||
}); | ||
|
||
describe("ExtractText", () => { | ||
it("should be able to extract one by one", () => { | ||
const textExtractor = new ExtractText("testText"); | ||
expect(textExtractor.extract(2)).toBe("te"); | ||
expect(textExtractor.extract(2)).toBe("st"); | ||
expect(textExtractor.extract(1)).toBe("T"); | ||
expect(textExtractor.extract(5)).toBe("ext"); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
export const chunkText = (text: string, length: number): string[] => { | ||
const regexChunk = new RegExp(`.{1,${length}}`, "g"); | ||
return text.match(regexChunk)!; | ||
}; | ||
|
||
export class ExtractText { | ||
constructor(private text: string) { | ||
} | ||
public extract(length: number): string { | ||
const text = this.text.substr(0, length); | ||
this.text = this.text.substr(length); | ||
return text; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters