// ───────────────────────────────────────────────────────────────────────────── // CODESIGNAL "INDUSTRY CODING" PROJECT SKELETONS — in-memory DB + banking system // ───────────────────────────────────────────────────────────────────────────── // SHAPE : one class, one Map as the source of truth, records are small objects // you can ADD FIELDS TO later, tiny private helpers, every public method // short. New levels arrive as OPTIONAL trailing params or NEW methods; // existing signatures never change, so level-1 tests keep passing. // TIME : `timestamp` is PASSED IN. Never read Date.now() inside. Tests are // deterministic and timestamps are strictly increasing between calls. // RETURNS : match the spec EXACTLY. These tasks love '' / 'true' / 'false' / // 'account1(500), account2(300)' string formats and null-vs-'' details. // SORT : "by X desc, ties by id asc" -> (a, b) => b.x - a.x || a.id.localeCompare(b.id) // SAY : "single source of truth", "backward compatible", "encapsulate", // "deterministic", "lazy expiry on read", "snapshot/restore" // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ═══ A. IN-MEMORY DATABASE (records -> fields -> values; TTL; backup/restore) ═══ class InMemoryDB { #records = new Map(); // key -> Map #backups = []; // [{ at, snapshot }] in time order // ── helpers ── #alive(cell, now) { return cell !== undefined && (cell.expiresAt === null || cell.expiresAt > now); } #liveFields(key, now) { const rec = this.#records.get(key); if (!rec) return []; return [...rec].filter(([, cell]) => this.#alive(cell, now)); } // ── level 1: set / get / delete ── set(key, field, value, timestamp = 0, ttl = null) { // ttl added at level 3 as an optional param if (!this.#records.has(key)) this.#records.set(key, new Map()); this.#records.get(key).set(field, { value, expiresAt: ttl === null ? null : timestamp + ttl }); return ''; } get(key, field, timestamp = 0) { const cell = this.#records.get(key)?.get(field); return this.#alive(cell, timestamp) ? cell.value : ''; // spec said '' on miss (sometimes null — read it!) } delete(key, field, timestamp = 0) { const rec = this.#records.get(key); const existed = this.#alive(rec?.get(field), timestamp); rec?.delete(field); if (rec && rec.size === 0) this.#records.delete(key); return existed ? 'true' : 'false'; } // ── level 2: scan / scanByPrefix -> "field1(value1), field2(value2)" sorted by field ── scan(key, timestamp = 0) { return this.scanByPrefix(key, '', timestamp); } scanByPrefix(key, prefix, timestamp = 0) { return this.#liveFields(key, timestamp) .filter(([f]) => f.startsWith(prefix)) .sort(([a], [b]) => a.localeCompare(b)) .map(([f, cell]) => `${f}(${cell.value})`) .join(', '); } // ── level 3: TTL — handled by the optional `ttl` on set() and `timestamp` on reads (lazy expiry) ── // ── level 4: backup / restore ── backup(timestamp) { const snapshot = []; for (const [key] of this.#records) { const fields = this.#liveFields(key, timestamp).map(([f, cell]) => [f, { value: cell.value, remainingTtl: cell.expiresAt === null ? null : cell.expiresAt - timestamp }]); if (fields.length) snapshot.push([key, fields]); } this.#backups.push({ at: timestamp, snapshot }); return String(snapshot.length); // number of non-empty records } restore(timestamp, restoreFrom) { const b = [...this.#backups].reverse().find(x => x.at <= restoreFrom); // latest at or before if (!b) return ''; this.#records = new Map(b.snapshot.map(([key, fields]) => [ key, new Map(fields.map(([f, s]) => [f, { value: s.value, expiresAt: s.remainingTtl === null ? null : timestamp + s.remainingTtl }])), ])); return ''; } } { const db = new InMemoryDB(); assert.equal(db.set('A', 'B', 'E'), ''); assert.equal(db.set('A', 'C', 'F'), ''); assert.equal(db.get('A', 'B'), 'E'); assert.equal(db.get('A', 'D'), ''); assert.equal(db.delete('A', 'B'), 'true'); assert.equal(db.delete('A', 'D'), 'false'); assert.equal(db.scan('A'), 'C(F)'); db.set('A', 'CA', 'G'); db.set('A', 'D', 'H'); assert.equal(db.scanByPrefix('A', 'C'), 'C(F), CA(G)'); assert.equal(db.scan('missing'), ''); // TTL db.set('T', 'x', '1', 100, 50); // alive for [100, 150) assert.equal(db.get('T', 'x', 149), '1'); assert.equal(db.get('T', 'x', 150), ''); assert.equal(db.delete('T', 'x', 150), 'false'); // backup/restore db.set('T', 'y', '2', 120, 30); // alive [120, 150) assert.equal(db.backup(130), '2'); // A and T (T.y has 20 left; T.x has 20 left) db.delete('A', 'C', 131); assert.equal(db.restore(200, 135), ''); assert.equal(db.get('A', 'C', 200), 'F'); assert.equal(db.get('T', 'y', 219), '2'); // 20 remaining from 200 assert.equal(db.get('T', 'y', 220), ''); } // ═══ B. BANKING SYSTEM (accounts, deposit/transfer, top spenders, scheduled payments w/ cashback, merge) ═══ class Bank { #accounts = new Map(); // id -> { balance, outgoing, createdAt, mergedInto } #payments = new Map(); // paymentId -> { from, to, amount, cashbackPct, dueAt, status } #paymentSeq = 0; #acct(id) { // follow merges so old ids keep working let a = this.#accounts.get(id); while (a?.mergedInto) a = this.#accounts.get(a.mergedInto); return a ?? null; } // ── level 1 ── createAccount(id, timestamp) { if (this.#accounts.has(id)) return false; this.#accounts.set(id, { id, balance: 0, outgoing: 0, createdAt: timestamp, mergedInto: null }); return true; } deposit(id, timestamp, amount) { this.processScheduledPayments(timestamp); // every op first settles what's due (level 3) const a = this.#acct(id); if (!a || amount <= 0) return null; a.balance += amount; return a.balance; } transfer(fromId, toId, timestamp, amount) { this.processScheduledPayments(timestamp); const from = this.#acct(fromId), to = this.#acct(toId); if (!from || !to || from === to || amount <= 0 || from.balance < amount) return null; from.balance -= amount; from.outgoing += amount; to.balance += amount; return from.balance; } // ── level 2: ranking ── topSpenders(timestamp, n) { this.processScheduledPayments(timestamp); return [...this.#accounts.values()] .filter(a => !a.mergedInto) .sort((a, b) => b.outgoing - a.outgoing || a.id.localeCompare(b.id)) .slice(0, n) .map(a => `${a.id}(${a.outgoing})`); } // ── level 3: scheduled payments with cashback ── schedulePayment(fromId, toId, timestamp, amount, cashbackPct, delay = 0) { this.processScheduledPayments(timestamp); if (!this.#acct(fromId) || !this.#acct(toId)) return null; const paymentId = `payment${++this.#paymentSeq}`; this.#payments.set(paymentId, { from: fromId, to: toId, amount, cashbackPct, dueAt: timestamp + delay, status: 'SCHEDULED' }); return paymentId; } processScheduledPayments(now) { // idempotent; call at the top of every op const due = [...this.#payments.entries()] .filter(([, p]) => p.status === 'SCHEDULED' && p.dueAt <= now) .sort(([ia, a], [ib, b]) => a.dueAt - b.dueAt || ia.localeCompare(ib)); // deterministic order for (const [, p] of due) { const from = this.#acct(p.from), to = this.#acct(p.to); if (from && to && from.balance >= p.amount) { from.balance -= p.amount; from.outgoing += p.amount; to.balance += p.amount; from.balance += Math.floor(p.amount * p.cashbackPct / 100); p.status = 'PROCESSED'; } else p.status = 'FAILED'; } } getPaymentStatus(accountId, timestamp, paymentId) { this.processScheduledPayments(timestamp); const p = this.#payments.get(paymentId); return p && this.#acct(p.from) === this.#acct(accountId) ? p.status : null; } // ── level 4: merge ── mergeAccounts(id1, id2, timestamp) { this.processScheduledPayments(timestamp); const a = this.#acct(id1), b = this.#acct(id2); if (!a || !b || a === b) return false; a.balance += b.balance; a.outgoing += b.outgoing; b.balance = 0; b.outgoing = 0; b.mergedInto = a.id; // keep b's id resolvable -> history stays valid return true; } } { const bank = new Bank(); assert.equal(bank.createAccount('acc1', 1), true); assert.equal(bank.createAccount('acc1', 2), false); assert.equal(bank.createAccount('acc2', 3), true); assert.equal(bank.deposit('acc1', 4, 1000), 1000); assert.equal(bank.deposit('nope', 5, 10), null); assert.equal(bank.transfer('acc1', 'acc2', 6, 300), 700); assert.equal(bank.transfer('acc1', 'acc2', 7, 5000), null); assert.equal(bank.transfer('acc1', 'acc1', 8, 1), null); assert.deepEqual(bank.topSpenders(9, 5), ['acc1(300)', 'acc2(0)']); const pid = bank.schedulePayment('acc1', 'acc2', 10, 100, 10, 5); // due at 15, 10% cashback assert.equal(pid, 'payment1'); assert.equal(bank.getPaymentStatus('acc1', 11, pid), 'SCHEDULED'); assert.equal(bank.getPaymentStatus('acc2', 11, pid), null); // not the payer assert.equal(bank.deposit('acc1', 15, 0), null); // triggers processing even though it fails assert.equal(bank.getPaymentStatus('acc1', 16, pid), 'PROCESSED'); assert.equal(bank.deposit('acc1', 17, 1), 700 - 100 + 10 + 1); // cashback landed assert.deepEqual(bank.topSpenders(18, 1), ['acc1(400)']); assert.equal(bank.mergeAccounts('acc1', 'acc2', 19), true); assert.equal(bank.deposit('acc2', 20, 1), 611 + 400 + 1); // acc2 now resolves to acc1 assert.deepEqual(bank.topSpenders(21, 5), ['acc1(400)']); } // ═══ C. CHECKLIST before you submit a level ═══ // [ ] every return matches the spec's TYPE and FORMAT ('' vs null vs 'false') // [ ] sort order + tie-breaker exactly as written // [ ] expired things are invisible to EVERY read path (get/scan/count/backup) // [ ] operations on missing ids/keys return the specified "miss" value, never throw // [ ] earlier-level tests still pass (run them again — you changed shared code) // [ ] processScheduled/expiry runs at the top of every op that takes a timestamp console.log('ok: javascript/ica-project-skeletons.js');