javascript
exercises
exercises.js⚡javascript
/**
* 10.7 Object.fromEntries and Advanced Object Methods - Exercises
*/
/**
* Exercise 1: Object Transform
* Create a function that transforms object values
*/
function transformValues(obj, transformer) {
// TODO: Apply transformer function to each value
// Return new object with transformed values
}
// Test:
// const nums = { a: 1, b: 2, c: 3 };
// console.log(transformValues(nums, n => n * 2)); // { a: 2, b: 4, c: 6 }
// console.log(transformValues(nums, n => n.toString())); // { a: '1', b: '2', c: '3' }
/**
* Exercise 2: Filter Object
* Create a function that filters object properties
*/
function filterObject(obj, predicate) {
// TODO: Keep only properties where predicate returns true
// Predicate receives (value, key)
}
// Test:
// const scores = { math: 85, english: 60, science: 92 };
// console.log(filterObject(scores, v => v > 70)); // { math: 85, science: 92 }
/**
* Exercise 3: Rename Keys
* Create a function that renames object keys
*/
function renameKeys(obj, keyMap) {
// TODO: Rename keys according to keyMap
// keyMap = { oldKey: 'newKey' }
}
// Test:
// const user = { firstName: 'John', lastName: 'Doe' };
// const renamed = renameKeys(user, { firstName: 'first', lastName: 'last' });
// console.log(renamed); // { first: 'John', last: 'Doe' }
/**
* Exercise 4: Group By with Transform
* Enhanced groupBy that can transform groups
*/
function groupByWithTransform(array, keyFn, transformFn = (x) => x) {
// TODO: Group array by keyFn
// Apply transformFn to each group
}
// Test:
// const products = [
// { name: 'Apple', category: 'fruit', price: 1.5 },
// { name: 'Banana', category: 'fruit', price: 0.75 },
// { name: 'Carrot', category: 'vegetable', price: 0.50 }
// ];
//
// const grouped = groupByWithTransform(
// products,
// p => p.category,
// group => group.reduce((sum, p) => sum + p.price, 0)
// );
// console.log(grouped); // { fruit: 2.25, vegetable: 0.50 }
/**
* Exercise 5: Deep Pick
* Pick nested properties from object
*/
function deepPick(obj, paths) {
// TODO: Pick properties at nested paths
// paths = ['user.name', 'user.email', 'settings.theme']
}
// Test:
// const data = {
// user: { name: 'John', email: 'john@example.com', password: 'secret' },
// settings: { theme: 'dark', notifications: true }
// };
// console.log(deepPick(data, ['user.name', 'settings.theme']));
// // { user: { name: 'John' }, settings: { theme: 'dark' } }
/**
* Exercise 6: Object Differ
* Create a comprehensive object diff utility
*/
function createDiffer() {
// TODO: Return an object with methods:
// - diff(obj1, obj2): Return { added, removed, changed }
// - patch(obj, diff): Apply diff to object
// - hasChanges(diff): Check if diff has any changes
}
// Test:
// const differ = createDiffer();
// const old = { a: 1, b: 2, c: 3 };
// const new_ = { a: 1, b: 5, d: 4 };
// const d = differ.diff(old, new_);
// console.log(differ.hasChanges(d)); // true
// console.log(differ.patch(old, d)); // { a: 1, b: 5, d: 4 }
/**
* Exercise 7: Query Builder
* Build URL query strings with object manipulation
*/
class QueryBuilder {
// TODO: Implement query string builder
// constructor(baseUrl)
// set(key, value): Set parameter
// setAll(params): Set multiple parameters
// remove(key): Remove parameter
// get(key): Get parameter value
// has(key): Check if parameter exists
// clear(): Remove all parameters
// toString(): Return full URL with query string
// toObject(): Return parameters as object
// static parse(url): Create from URL string
}
// Test:
// const qb = new QueryBuilder('https://api.example.com/search');
// qb.set('q', 'javascript').set('page', 1).set('limit', 10);
// console.log(qb.toString());
// // "https://api.example.com/search?q=javascript&page=1&limit=10"
/**
* Exercise 8: Schema Validator
* Validate object against schema
*/
function createValidator(schema) {
// TODO: Return validation function
// Schema format:
// {
// name: { type: 'string', required: true },
// age: { type: 'number', min: 0, max: 150 },
// email: { type: 'string', pattern: /^.+@.+$/ }
// }
// Return { valid: boolean, errors: string[] }
}
// Test:
// const validate = createValidator({
// name: { type: 'string', required: true },
// age: { type: 'number', min: 0 }
// });
// console.log(validate({ name: 'John', age: 30 })); // { valid: true, errors: [] }
// console.log(validate({ age: -5 })); // { valid: false, errors: [...] }
/**
* Exercise 9: Object Cache
* Create a cached object store
*/
class ObjectCache {
// TODO: Implement cache with:
// constructor(options): { maxSize, ttl }
// set(key, value): Store value
// get(key): Get value (null if expired/missing)
// has(key): Check if key exists and not expired
// delete(key): Remove key
// clear(): Remove all
// entries(): Return all valid entries
// keys(): Return all valid keys
// values(): Return all valid values
// cleanup(): Remove expired entries
// stats(): Return { size, hits, misses }
}
// Test:
// const cache = new ObjectCache({ maxSize: 100, ttl: 60000 });
// cache.set('user:1', { name: 'John' });
// console.log(cache.get('user:1'));
/**
* Exercise 10: Object Projection
* Project and reshape objects
*/
function project(obj, projection) {
// TODO: Reshape object according to projection
// Projection supports:
// - Simple mapping: { newKey: 'oldKey' }
// - Nested paths: { name: 'user.name' }
// - Computed: { fullName: obj => obj.first + ' ' + obj.last }
// - Array mapping: { 'tags[]': 'categories[].name' }
}
// Test:
// const data = {
// user: { first: 'John', last: 'Doe' },
// metadata: { created: '2024-01-01', updated: '2024-03-15' }
// };
//
// const result = project(data, {
// firstName: 'user.first',
// lastName: 'user.last',
// fullName: obj => `${obj.user.first} ${obj.user.last}`,
// dates: { created: 'metadata.created' }
// });
/**
* BONUS CHALLENGES
*/
/**
* Bonus 1: Immutable Object Updates
* Create helper for immutable nested updates
*/
function immutableUpdate(obj, path, value) {
// TODO: Return new object with updated nested value
// Path is dot-separated: 'user.settings.theme'
}
/**
* Bonus 2: Object Flattening and Unflattening
* Convert between nested and flat objects
*/
function flatten(obj, prefix = '') {
// TODO: { a: { b: 1 } } -> { 'a.b': 1 }
}
function unflatten(obj) {
// TODO: { 'a.b': 1 } -> { a: { b: 1 } }
}
/**
* Bonus 3: Object Observable
* Create observable wrapper for objects
*/
function createObservable(obj) {
// TODO: Return proxy that emits events on changes
// - on(event, handler): Subscribe to events
// - off(event, handler): Unsubscribe
// Events: 'set', 'delete', 'change'
}
// ============================================
// SOLUTION KEY (for reference)
// ============================================
/*
// Exercise 1 Solution:
function transformValues(obj, transformer) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, transformer(value, key)])
);
}
// Exercise 2 Solution:
function filterObject(obj, predicate) {
return Object.fromEntries(
Object.entries(obj).filter(([key, value]) => predicate(value, key))
);
}
// Exercise 3 Solution:
function renameKeys(obj, keyMap) {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [keyMap[key] || key, value])
);
}
// Exercise 4 Solution:
function groupByWithTransform(array, keyFn, transformFn = x => x) {
const groups = array.reduce((acc, item) => {
const key = keyFn(item);
(acc[key] ??= []).push(item);
return acc;
}, {});
return Object.fromEntries(
Object.entries(groups).map(([key, group]) => [key, transformFn(group)])
);
}
// Exercise 5 Solution:
function deepPick(obj, paths) {
const result = {};
for (const path of paths) {
const parts = path.split('.');
let source = obj;
let target = result;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (source === undefined) break;
if (i === parts.length - 1) {
target[part] = source[part];
} else {
target[part] ??= {};
target = target[part];
source = source[part];
}
}
}
return result;
}
// Exercise 6 Solution:
function createDiffer() {
return {
diff(obj1, obj2) {
const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
const added = {}, removed = {}, changed = {};
for (const key of keys) {
if (!(key in obj1)) added[key] = obj2[key];
else if (!(key in obj2)) removed[key] = obj1[key];
else if (obj1[key] !== obj2[key]) {
changed[key] = { from: obj1[key], to: obj2[key] };
}
}
return { added, removed, changed };
},
patch(obj, diff) {
const result = { ...obj };
for (const key of Object.keys(diff.removed)) delete result[key];
Object.assign(result, diff.added);
for (const [key, { to }] of Object.entries(diff.changed)) {
result[key] = to;
}
return result;
},
hasChanges(diff) {
return Object.keys(diff.added).length > 0 ||
Object.keys(diff.removed).length > 0 ||
Object.keys(diff.changed).length > 0;
}
};
}
// Exercise 7 Solution:
class QueryBuilder {
constructor(baseUrl) {
const url = new URL(baseUrl);
this.baseUrl = url.origin + url.pathname;
this.params = Object.fromEntries(url.searchParams);
}
set(key, value) { this.params[key] = value; return this; }
setAll(params) { Object.assign(this.params, params); return this; }
remove(key) { delete this.params[key]; return this; }
get(key) { return this.params[key]; }
has(key) { return key in this.params; }
clear() { this.params = {}; return this; }
toString() {
const qs = new URLSearchParams(this.params).toString();
return qs ? `${this.baseUrl}?${qs}` : this.baseUrl;
}
toObject() { return { ...this.params }; }
static parse(url) { return new QueryBuilder(url); }
}
*/
console.log('Complete the exercises above!');
console.log('Check the solution key at the bottom for guidance.');