javascript

exercises

exercises.js
/**
 * ========================================
 * 11.2 Working with JSON - Exercises
 * ========================================
 *
 * Practice JSON parsing, stringifying, and manipulation.
 * Complete each exercise by filling in the code.
 */

/**
 * EXERCISE 1: Basic JSON Conversion
 *
 * Convert between JavaScript objects and JSON strings.
 */

function objectToJSON(obj) {
  // Convert object to JSON string
}

function jsonToObject(jsonString) {
  // Convert JSON string to object
  // Return null if invalid JSON
}

// Test:
// objectToJSON({ name: 'John', age: 30 }) → '{"name":"John","age":30}'
// jsonToObject('{"name":"John"}') → { name: 'John' }
// jsonToObject('invalid') → null

/**
 * EXERCISE 2: Pretty Print JSON
 *
 * Create a function that formats JSON with custom indentation.
 */

function prettyJSON(obj, indent = 2) {
  // Return prettified JSON string
}

// Test:
// prettyJSON({ a: 1, b: 2 }, 4) should return nicely formatted string

/**
 * EXERCISE 3: Filter Sensitive Data
 *
 * Create a function that removes sensitive fields from JSON.
 */

function sanitizeJSON(obj, sensitiveFields = ['password', 'ssn', 'token']) {
  // Return JSON string with sensitive fields removed
}

// Test:
// sanitizeJSON({ user: 'john', password: 'secret' })
// → '{"user":"john"}'

/**
 * EXERCISE 4: Date-Aware JSON
 *
 * Create stringify/parse functions that handle dates properly.
 */

function stringifyWithDates(obj) {
  // Stringify object, dates should become ISO strings
}

function parseWithDates(jsonString) {
  // Parse JSON, converting ISO date strings back to Date objects
}

// Test:
// const obj = { event: 'party', date: new Date('2024-12-31') };
// const json = stringifyWithDates(obj);
// const parsed = parseWithDates(json);
// parsed.date instanceof Date → true

/**
 * EXERCISE 5: JSON Path Accessor
 *
 * Access nested values using dot notation path.
 */

function getJSONPath(obj, path) {
  // Get value at path (e.g., 'user.address.city')
  // Return undefined if path doesn't exist
}

function setJSONPath(obj, path, value) {
  // Set value at path, creating nested objects if needed
  // Return modified object
}

// Test:
// getJSONPath({ user: { name: 'John' } }, 'user.name') → 'John'
// setJSONPath({}, 'user.address.city', 'NYC')
// → { user: { address: { city: 'NYC' } } }

/**
 * EXERCISE 6: Safe Circular JSON
 *
 * Stringify objects with circular references.
 */

function safeStringify(obj, space = 2) {
  // Stringify even with circular references
  // Replace circular refs with '[Circular]'
}

// Test:
// const obj = { name: 'test' };
// obj.self = obj;
// safeStringify(obj) should not throw

/**
 * EXERCISE 7: JSON Schema Validator
 *
 * Validate JSON against a schema.
 */

function validateSchema(data, schema) {
  // Return { valid: boolean, errors: string[] }
  // Schema format:
  // {
  //   fieldName: {
  //     type: 'string' | 'number' | 'boolean' | 'array' | 'object',
  //     required: boolean,
  //     min: number (for numbers/strings/arrays),
  //     max: number,
  //     pattern: RegExp (for strings)
  //   }
  // }
}

// Test:
// const schema = {
//   name: { type: 'string', required: true, min: 2 },
//   age: { type: 'number', required: true, min: 0, max: 150 }
// };
// validateSchema({ name: 'J', age: 200 }, schema)
// → { valid: false, errors: [...] }

/**
 * EXERCISE 8: JSON Merge
 *
 * Deep merge multiple JSON objects.
 */

function mergeJSON(...objects) {
  // Deep merge objects (later objects override earlier)
  // Arrays should be replaced, not merged
}

// Test:
// mergeJSON(
//   { a: 1, b: { c: 2 } },
//   { b: { d: 3 }, e: 4 }
// )
// → { a: 1, b: { c: 2, d: 3 }, e: 4 }

/**
 * EXERCISE 9: JSON Diff
 *
 * Find differences between two objects.
 */

function diffJSON(obj1, obj2) {
  // Return array of differences:
  // [{ path: string, type: 'added' | 'removed' | 'changed', oldValue?, newValue? }]
}

// Test:
// diffJSON({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 })
// → [
//     { path: 'b', type: 'changed', oldValue: 2, newValue: 3 },
//     { path: 'c', type: 'added', newValue: 4 }
//   ]

/**
 * EXERCISE 10: JSON Flattener
 *
 * Flatten nested objects to single-level with dot notation keys.
 */

function flattenJSON(obj, prefix = '') {
  // Flatten nested object
  // { a: { b: { c: 1 } } } → { 'a.b.c': 1 }
}

function unflattenJSON(flatObj) {
  // Reverse: { 'a.b.c': 1 } → { a: { b: { c: 1 } } }
}

// Test:
// flattenJSON({ user: { name: 'John', address: { city: 'NYC' } } })
// → { 'user.name': 'John', 'user.address.city': 'NYC' }

/**
 * EXERCISE 11: Collection Serializer
 *
 * Serialize/deserialize Maps and Sets.
 */

function serializeCollections(obj) {
  // Stringify with Map/Set support
}

function deserializeCollections(json) {
  // Parse with Map/Set restoration
}

// Test:
// const obj = { myMap: new Map([['a', 1]]), mySet: new Set([1,2,3]) };
// const json = serializeCollections(obj);
// const restored = deserializeCollections(json);
// restored.myMap instanceof Map → true

/**
 * EXERCISE 12: JSON Query
 *
 * Query JSON like a database.
 */

function queryJSON(data, query) {
  // data: array of objects
  // query: { field: value } or { field: { $gt: n, $lt: n, $in: [] } }
  // Return matching items
}

// Test:
// const users = [
//   { name: 'Alice', age: 25 },
//   { name: 'Bob', age: 30 },
//   { name: 'Charlie', age: 35 }
// ];
// queryJSON(users, { age: { $gt: 25 } }) → [Bob, Charlie]
// queryJSON(users, { name: { $in: ['Alice', 'Bob'] } }) → [Alice, Bob]

/**
 * EXERCISE 13: JSON Transformer
 *
 * Transform JSON structure using a mapping.
 */

function transformJSON(obj, mapping) {
  // mapping: { newKey: 'old.path' } or { newKey: (obj) => value }
  // Transform object according to mapping
}

// Test:
// transformJSON(
//   { user: { firstName: 'John', lastName: 'Doe' }, age: 30 },
//   {
//     fullName: obj => `${obj.user.firstName} ${obj.user.lastName}`,
//     userAge: 'age',
//     city: 'user.address.city' // undefined if doesn't exist
//   }
// )
// → { fullName: 'John Doe', userAge: 30, city: undefined }

/**
 * EXERCISE 14: JSON Patch
 *
 * Apply JSON Patch operations (RFC 6902 simplified).
 */

function applyJSONPatch(obj, patches) {
  // patches: array of { op: 'add'|'remove'|'replace', path: string, value?: any }
  // Apply patches to object and return result
}

// Test:
// applyJSONPatch(
//   { name: 'John', age: 30 },
//   [
//     { op: 'replace', path: '/name', value: 'Jane' },
//     { op: 'add', path: '/email', value: 'jane@example.com' },
//     { op: 'remove', path: '/age' }
//   ]
// )
// → { name: 'Jane', email: 'jane@example.com' }

/**
 * EXERCISE 15: Streaming JSON Parser
 *
 * Parse large JSON-like data incrementally.
 */

class JSONStreamParser {
  constructor() {
    this.buffer = '';
    this.objects = [];
  }

  write(chunk) {
    // Add chunk to buffer
    // Try to extract complete JSON objects
    // Return array of parsed objects
  }

  end() {
    // Flush any remaining data
    // Return final parsed objects or throw if incomplete
  }
}

// Test:
// const parser = new JSONStreamParser();
// parser.write('{"name":"Jo');
// parser.write('hn"}{"age":');
// parser.write('30}');
// parser.end();
// → [{ name: 'John' }, { age: 30 }]

// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================

/*
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 * SOLUTIONS BELOW
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

// SOLUTION 1: Basic JSON Conversion
function objectToJSONSolution(obj) {
  return JSON.stringify(obj);
}

function jsonToObjectSolution(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch {
    return null;
  }
}

// SOLUTION 2: Pretty Print JSON
function prettyJSONSolution(obj, indent = 2) {
  return JSON.stringify(obj, null, indent);
}

// SOLUTION 3: Filter Sensitive Data
function sanitizeJSONSolution(
  obj,
  sensitiveFields = ['password', 'ssn', 'token']
) {
  return JSON.stringify(obj, (key, value) => {
    if (sensitiveFields.includes(key)) {
      return undefined;
    }
    return value;
  });
}

// SOLUTION 4: Date-Aware JSON
function stringifyWithDatesSolution(obj) {
  return JSON.stringify(obj);
}

function parseWithDatesSolution(jsonString) {
  const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;

  return JSON.parse(jsonString, (key, value) => {
    if (typeof value === 'string' && isoDateRegex.test(value)) {
      return new Date(value);
    }
    return value;
  });
}

// SOLUTION 5: JSON Path Accessor
function getJSONPathSolution(obj, path) {
  const parts = path.split('.');
  let current = obj;

  for (const part of parts) {
    if (current === null || current === undefined) {
      return undefined;
    }
    current = current[part];
  }

  return current;
}

function setJSONPathSolution(obj, path, value) {
  const parts = path.split('.');
  let current = obj;

  for (let i = 0; i < parts.length - 1; i++) {
    const part = parts[i];
    if (!(part in current) || typeof current[part] !== 'object') {
      current[part] = {};
    }
    current = current[part];
  }

  current[parts[parts.length - 1]] = value;
  return obj;
}

// SOLUTION 6: Safe Circular JSON
function safeStringifySolution(obj, space = 2) {
  const seen = new WeakSet();

  return JSON.stringify(
    obj,
    (key, value) => {
      if (typeof value === 'object' && value !== null) {
        if (seen.has(value)) {
          return '[Circular]';
        }
        seen.add(value);
      }
      return value;
    },
    space
  );
}

// SOLUTION 7: JSON Schema Validator
function validateSchemaSolution(data, schema) {
  const errors = [];

  for (const [field, rules] of Object.entries(schema)) {
    const value = data[field];

    // Required check
    if (rules.required && (value === undefined || value === null)) {
      errors.push(`${field} is required`);
      continue;
    }

    if (value === undefined || value === null) continue;

    // Type check
    if (rules.type) {
      const actualType = Array.isArray(value) ? 'array' : typeof value;
      if (actualType !== rules.type) {
        errors.push(`${field} must be type ${rules.type}`);
        continue;
      }
    }

    // Min/Max for numbers
    if (typeof value === 'number') {
      if (rules.min !== undefined && value < rules.min) {
        errors.push(`${field} must be >= ${rules.min}`);
      }
      if (rules.max !== undefined && value > rules.max) {
        errors.push(`${field} must be <= ${rules.max}`);
      }
    }

    // Min/Max for strings (length)
    if (typeof value === 'string') {
      if (rules.min !== undefined && value.length < rules.min) {
        errors.push(`${field} must be at least ${rules.min} characters`);
      }
      if (rules.max !== undefined && value.length > rules.max) {
        errors.push(`${field} must be at most ${rules.max} characters`);
      }
      if (rules.pattern && !rules.pattern.test(value)) {
        errors.push(`${field} has invalid format`);
      }
    }

    // Min/Max for arrays (length)
    if (Array.isArray(value)) {
      if (rules.min !== undefined && value.length < rules.min) {
        errors.push(`${field} must have at least ${rules.min} items`);
      }
      if (rules.max !== undefined && value.length > rules.max) {
        errors.push(`${field} must have at most ${rules.max} items`);
      }
    }
  }

  return { valid: errors.length === 0, errors };
}

// SOLUTION 8: JSON Merge
function mergeJSONSolution(...objects) {
  function deepMerge(target, source) {
    const result = { ...target };

    for (const key of Object.keys(source)) {
      const targetValue = result[key];
      const sourceValue = source[key];

      if (
        typeof targetValue === 'object' &&
        targetValue !== null &&
        !Array.isArray(targetValue) &&
        typeof sourceValue === 'object' &&
        sourceValue !== null &&
        !Array.isArray(sourceValue)
      ) {
        result[key] = deepMerge(targetValue, sourceValue);
      } else {
        result[key] = sourceValue;
      }
    }

    return result;
  }

  return objects.reduce((acc, obj) => deepMerge(acc, obj), {});
}

// SOLUTION 9: JSON Diff
function diffJSONSolution(obj1, obj2, path = '') {
  const differences = [];
  const allKeys = new Set([
    ...Object.keys(obj1 || {}),
    ...Object.keys(obj2 || {}),
  ]);

  for (const key of allKeys) {
    const fullPath = path ? `${path}.${key}` : key;
    const val1 = obj1?.[key];
    const val2 = obj2?.[key];

    if (val1 === undefined && val2 !== undefined) {
      differences.push({ path: fullPath, type: 'added', newValue: val2 });
    } else if (val1 !== undefined && val2 === undefined) {
      differences.push({ path: fullPath, type: 'removed', oldValue: val1 });
    } else if (
      typeof val1 === 'object' &&
      val1 !== null &&
      typeof val2 === 'object' &&
      val2 !== null
    ) {
      differences.push(...diffJSONSolution(val1, val2, fullPath));
    } else if (val1 !== val2) {
      differences.push({
        path: fullPath,
        type: 'changed',
        oldValue: val1,
        newValue: val2,
      });
    }
  }

  return differences;
}

// SOLUTION 10: JSON Flattener
function flattenJSONSolution(obj, prefix = '') {
  const result = {};

  for (const [key, value] of Object.entries(obj)) {
    const newKey = prefix ? `${prefix}.${key}` : key;

    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      Object.assign(result, flattenJSONSolution(value, newKey));
    } else {
      result[newKey] = value;
    }
  }

  return result;
}

function unflattenJSONSolution(flatObj) {
  const result = {};

  for (const [path, value] of Object.entries(flatObj)) {
    const parts = path.split('.');
    let current = result;

    for (let i = 0; i < parts.length - 1; i++) {
      if (!(parts[i] in current)) {
        current[parts[i]] = {};
      }
      current = current[parts[i]];
    }

    current[parts[parts.length - 1]] = value;
  }

  return result;
}

// SOLUTION 11: Collection Serializer
function serializeCollectionsSolution(obj) {
  return JSON.stringify(obj, (key, value) => {
    if (value instanceof Map) {
      return { __type__: 'Map', __data__: [...value.entries()] };
    }
    if (value instanceof Set) {
      return { __type__: 'Set', __data__: [...value] };
    }
    return value;
  });
}

function deserializeCollectionsSolution(json) {
  return JSON.parse(json, (key, value) => {
    if (value && typeof value === 'object') {
      if (value.__type__ === 'Map') return new Map(value.__data__);
      if (value.__type__ === 'Set') return new Set(value.__data__);
    }
    return value;
  });
}

// SOLUTION 12: JSON Query
function queryJSONSolution(data, query) {
  return data.filter((item) => {
    for (const [field, condition] of Object.entries(query)) {
      const value = item[field];

      if (typeof condition === 'object' && condition !== null) {
        // Handle operators
        if (condition.$gt !== undefined && !(value > condition.$gt))
          return false;
        if (condition.$gte !== undefined && !(value >= condition.$gte))
          return false;
        if (condition.$lt !== undefined && !(value < condition.$lt))
          return false;
        if (condition.$lte !== undefined && !(value <= condition.$lte))
          return false;
        if (condition.$eq !== undefined && value !== condition.$eq)
          return false;
        if (condition.$ne !== undefined && value === condition.$ne)
          return false;
        if (condition.$in !== undefined && !condition.$in.includes(value))
          return false;
        if (condition.$nin !== undefined && condition.$nin.includes(value))
          return false;
      } else {
        // Direct equality
        if (value !== condition) return false;
      }
    }
    return true;
  });
}

// SOLUTION 13: JSON Transformer
function transformJSONSolution(obj, mapping) {
  const result = {};

  for (const [newKey, source] of Object.entries(mapping)) {
    if (typeof source === 'function') {
      result[newKey] = source(obj);
    } else if (typeof source === 'string') {
      result[newKey] = getJSONPathSolution(obj, source);
    } else {
      result[newKey] = source;
    }
  }

  return result;
}

// SOLUTION 14: JSON Patch
function applyJSONPatchSolution(obj, patches) {
  const result = JSON.parse(JSON.stringify(obj));

  for (const patch of patches) {
    const pathParts = patch.path.split('/').filter(Boolean);

    if (patch.op === 'add' || patch.op === 'replace') {
      let current = result;
      for (let i = 0; i < pathParts.length - 1; i++) {
        current = current[pathParts[i]];
      }
      current[pathParts[pathParts.length - 1]] = patch.value;
    } else if (patch.op === 'remove') {
      let current = result;
      for (let i = 0; i < pathParts.length - 1; i++) {
        current = current[pathParts[i]];
      }
      delete current[pathParts[pathParts.length - 1]];
    }
  }

  return result;
}

// SOLUTION 15: Streaming JSON Parser
class JSONStreamParserSolution {
  constructor() {
    this.buffer = '';
    this.objects = [];
  }

  write(chunk) {
    this.buffer += chunk;
    const extracted = [];

    let braceCount = 0;
    let startIndex = -1;

    for (let i = 0; i < this.buffer.length; i++) {
      const char = this.buffer[i];

      if (char === '{') {
        if (braceCount === 0) startIndex = i;
        braceCount++;
      } else if (char === '}') {
        braceCount--;
        if (braceCount === 0 && startIndex !== -1) {
          const jsonStr = this.buffer.substring(startIndex, i + 1);
          try {
            extracted.push(JSON.parse(jsonStr));
            this.objects.push(extracted[extracted.length - 1]);
          } catch {
            // Incomplete or invalid JSON
          }
          startIndex = -1;
        }
      }
    }

    // Keep only unparsed data in buffer
    if (startIndex !== -1) {
      this.buffer = this.buffer.substring(startIndex);
    } else {
      this.buffer = '';
    }

    return extracted;
  }

  end() {
    if (this.buffer.trim()) {
      try {
        const obj = JSON.parse(this.buffer);
        this.objects.push(obj);
      } catch (e) {
        throw new Error('Incomplete JSON data');
      }
    }
    return this.objects;
  }
}

console.log('Working with JSON exercises loaded!');
console.log(
  'Complete each exercise and check against solutions at the bottom.'
);
Exercises - JavaScript Tutorial | DeepML