Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
11 changes: 11 additions & 0 deletions HumanLiker0.5/backend/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"testEnvironment": "node",
"testMatch": [
"**/tests/**/*.test.js"
],
"collectCoverageFrom": [
"src/core/**/*.js"
],
"transform": {},
"extensionsToTreatAsEsm": [".js"]
}
18 changes: 10 additions & 8 deletions HumanLiker0.5/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "NODE_OPTIONS=--experimental-vm-modules jest"
},
"keywords": [
"humanliker",
Expand All @@ -17,19 +17,21 @@
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"better-sqlite3": "^11.7.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"winston": "^3.11.0",
"drizzle-orm": "^0.29.0",
"better-sqlite3": "^11.7.0",
"drizzle-kit": "^0.20.6",
"zod": "^3.22.4",
"drizzle-orm": "^0.29.0",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"winston": "^3.11.0",
"zod": "^3.22.4"
},
"devDependencies": {
"jest": "^29.7.0"
},
"engines": {
"node": ">=18.0.0 <=20.x"
}
}

138 changes: 138 additions & 0 deletions HumanLiker0.5/backend/src/core/ByteSize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* ByteSize - Represent and humanize byte sizes
* Example: 1024 bytes -> "1 KB"
*/

class ByteSize {
/**
* Create a ByteSize instance
* @param {number} bytes - Number of bytes
*/
constructor(bytes) {
if (typeof bytes !== 'number' || isNaN(bytes) || bytes < 0) {
throw new TypeError('Bytes must be a non-negative number');
}
this.bytes = Math.floor(bytes);
}

/**
* Create ByteSize from bytes
* @param {number} bytes - Number of bytes
* @returns {ByteSize} ByteSize instance
*/
static fromBytes(bytes) {
return new ByteSize(bytes);
}

/**
* Create ByteSize from kilobytes
* @param {number} kilobytes - Number of kilobytes
* @returns {ByteSize} ByteSize instance
*/
static fromKilobytes(kilobytes) {
return new ByteSize(kilobytes * 1024);
}

/**
* Create ByteSize from megabytes
* @param {number} megabytes - Number of megabytes
* @returns {ByteSize} ByteSize instance
*/
static fromMegabytes(megabytes) {
return new ByteSize(megabytes * 1024 * 1024);
}

/**
* Create ByteSize from gigabytes
* @param {number} gigabytes - Number of gigabytes
* @returns {ByteSize} ByteSize instance
*/
static fromGigabytes(gigabytes) {
return new ByteSize(gigabytes * 1024 * 1024 * 1024);
}

/**
* Create ByteSize from terabytes
* @param {number} terabytes - Number of terabytes
* @returns {ByteSize} ByteSize instance
*/
static fromTerabytes(terabytes) {
return new ByteSize(terabytes * 1024 * 1024 * 1024 * 1024);
}

/**
* Get kilobytes
* @returns {number} Size in kilobytes
*/
get kilobytes() {
return this.bytes / 1024;
}

/**
* Get megabytes
* @returns {number} Size in megabytes
*/
get megabytes() {
return this.bytes / (1024 * 1024);
}

/**
* Get gigabytes
* @returns {number} Size in gigabytes
*/
get gigabytes() {
return this.bytes / (1024 * 1024 * 1024);
}

/**
* Get terabytes
* @returns {number} Size in terabytes
*/
get terabytes() {
return this.bytes / (1024 * 1024 * 1024 * 1024);
}

/**
* Humanize the byte size
* @param {number} [precision=2] - Number of decimal places
* @returns {string} Human-readable size (e.g., "1.5 MB")
*/
humanize(precision = 2) {
if (typeof precision !== 'number' || precision < 0) {
precision = 2;
}

const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
let size = this.bytes;
let unitIndex = 0;

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}

const formatted = unitIndex === 0 ? size.toString() : size.toFixed(precision);
return `${formatted} ${units[unitIndex]}`;
}

/**
* Convert to string
* @returns {string} String representation
*/
toString() {
return this.humanize();
}

/**
* Convert to JSON
* @returns {object} JSON representation
*/
toJSON() {
return {
bytes: this.bytes,
humanized: this.humanize()
};
}
}

export default ByteSize;
39 changes: 39 additions & 0 deletions HumanLiker0.5/backend/src/core/CollectionHumanizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* CollectionHumanizer - Format arrays into human-readable strings
* Example: ["apple", "banana", "cherry"] -> "apple, banana and cherry"
*/

/**
* Humanize an array into a readable string
* @param {Array} array - The array to humanize
* @param {string} [separator=", "] - The separator between items
* @param {string} [lastSeparator=" and "] - The separator before the last item
* @returns {string} The humanized string
*/
function humanize(array, separator = ', ', lastSeparator = ' and ') {
if (!Array.isArray(array)) {
throw new TypeError('Input must be an array');
}

if (array.length === 0) {
return '';
}

if (array.length === 1) {
return String(array[0]);
}

if (array.length === 2) {
return `${array[0]}${lastSeparator}${array[1]}`;
}

// For 3 or more items
const allButLast = array.slice(0, -1);
const last = array[array.length - 1];

return allButLast.join(separator) + lastSeparator + last;
}

export default {
humanize
};
78 changes: 78 additions & 0 deletions HumanLiker0.5/backend/src/core/DateTimeHumanizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* DateTimeHumanizer - Convert dates to human-readable relative time strings
* Example: "2 hours ago", "in 3 days"
*/

/**
* Humanize a date relative to now
* @param {Date|string|number} date - The date to humanize
* @param {Date|string|number} [now=new Date()] - The reference date (defaults to current time)
* @returns {string} Human-readable time difference
*/
function humanize(date, now = new Date()) {
// Parse input dates
const targetDate = parseDate(date);
const referenceDate = parseDate(now);

if (!targetDate || !referenceDate) {
throw new TypeError('Invalid date provided');
}

// Calculate difference in milliseconds
const diffMs = targetDate - referenceDate;
const absDiffMs = Math.abs(diffMs);
const isPast = diffMs < 0;

// Define time units in milliseconds
const units = [
{ name: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
{ name: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
{ name: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
{ name: 'day', ms: 24 * 60 * 60 * 1000 },
{ name: 'hour', ms: 60 * 60 * 1000 },
{ name: 'minute', ms: 60 * 1000 },
{ name: 'second', ms: 1000 }
];

// Find the appropriate unit
for (const unit of units) {
const value = Math.floor(absDiffMs / unit.ms);

if (value >= 1) {
const plural = value > 1 ? 's' : '';
const timeStr = `${value} ${unit.name}${plural}`;

return isPast ? `${timeStr} ago` : `in ${timeStr}`;
}
}

// If less than a second
return 'just now';
}

/**
* Parse various date formats into Date object
* @param {Date|string|number} input - The date input
* @returns {Date|null} Parsed Date object or null if invalid
*/
function parseDate(input) {
if (input instanceof Date) {
return isNaN(input.getTime()) ? null : input;
}

if (typeof input === 'number') {
const date = new Date(input);
return isNaN(date.getTime()) ? null : date;
}

if (typeof input === 'string') {
const date = new Date(input);
return isNaN(date.getTime()) ? null : date;
}

return null;
}

export default {
humanize
};
86 changes: 86 additions & 0 deletions HumanLiker0.5/backend/src/core/MetricNumeral.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* MetricNumeral - Convert between standard numbers and metric notation
* Example: 1000 -> 1k, 1000000 -> 1M
*/

/**
* Convert a number to metric notation
* @param {number} num - The number to convert
* @param {number} [precision=1] - Number of decimal places
* @returns {string} The metric notation (e.g., "1.5k", "2.3M")
*/
function toMetric(num, precision = 1) {
if (typeof num !== 'number' || isNaN(num)) {
throw new TypeError('Input must be a valid number');
}

if (typeof precision !== 'number' || precision < 0) {
precision = 1;
}

const absNum = Math.abs(num);
const sign = num < 0 ? '-' : '';

const metricPrefixes = [
{ value: 1e12, symbol: 'T' }, // Tera
{ value: 1e9, symbol: 'G' }, // Giga
{ value: 1e6, symbol: 'M' }, // Mega
{ value: 1e3, symbol: 'k' } // Kilo
];

for (const { value, symbol } of metricPrefixes) {
if (absNum >= value) {
const converted = num / value;
return sign + converted.toFixed(precision) + symbol;
}
}

// Return as-is if less than 1000
return num.toFixed(precision);
}

/**
* Convert metric notation to standard number
* @param {string} metricString - The metric notation string (e.g., "1.5k", "2M")
* @returns {number} The standard number
*/
function fromMetric(metricString) {
if (typeof metricString !== 'string') {
throw new TypeError('Input must be a string');
}

const trimmed = metricString.trim();

if (!trimmed) {
throw new Error('Input string cannot be empty');
}

const metricSuffixes = {
'k': 1e3, // Kilo
'K': 1e3,
'M': 1e6, // Mega
'G': 1e9, // Giga
'T': 1e12 // Tera
};

// Match number with optional metric suffix
const match = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*([kKMGT]?)$/);

if (!match) {
throw new Error('Invalid metric format');
}

const numValue = parseFloat(match[1]);
const suffix = match[2];

if (suffix && metricSuffixes[suffix]) {
return numValue * metricSuffixes[suffix];
}

return numValue;
}

export default {
toMetric,
fromMetric
};
Loading