Implement a multi-level digital recipe manager
Company: Ramp
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Take-home Project
Quick Answer: Tests skills in data modeling and stateful class implementation, covering CRUD operations, case-insensitive string handling, recipe ID generation, list management, search and sort logic, and basic user/edit permissions.
Part 1: Simulate Basic Recipe CRUD
Constraints
- 0 <= len(operations) <= 1000
- 0 <= len(ingredients), len(steps) <= 100 per operation
- Recipe names are unique case-insensitively among existing recipes
- Recipe IDs in queries may refer to missing recipes
Examples
Input: [['add_recipe', 'Pasta', ['noodles', 'salt'], ['boil', 'mix']], ['get_recipe', 'recipe1'], ['update_recipe', 'recipe1', 'Cream Pasta', ['noodles', 'cream'], ['boil', 'stir']], ['get_recipe', 'recipe1'], ['delete_recipe', 'recipe1'], ['get_recipe', 'recipe1']]
Expected Output: ['recipe1', ['Pasta', 'noodles,salt', 'boil,mix'], True, ['Cream Pasta', 'noodles,cream', 'boil,stir'], True, []]
Explanation: Basic add, read, update, and delete flow.
Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_recipe', 'soup', ['water', 'salt'], ['boil']], ['add_recipe', 'Salad', ['lettuce'], ['mix']], ['update_recipe', 'recipe2', 'SOUP', ['lettuce'], ['mix']], ['get_recipe', 'recipe2']]
Expected Output: ['recipe1', None, 'recipe2', False, ['Salad', 'lettuce', 'mix']]
Explanation: Recipe names are unique case-insensitively, both when adding and updating.
Input: [['add_recipe', 'Tea', [], []], ['get_recipe', 'recipe1'], ['delete_recipe', 'recipe2'], ['update_recipe', 'recipe3', 'X', [], []]]
Expected Output: ['recipe1', ['Tea', '', ''], False, False]
Explanation: Edge case with empty ingredient/step lists and missing recipe IDs.
Input: [['add_recipe', 'A', ['x'], ['s1']], ['delete_recipe', 'recipe1'], ['add_recipe', 'B', ['y'], ['z']], ['get_recipe', 'recipe2']]
Expected Output: ['recipe1', True, 'recipe2', ['B', 'y', 'z']]
Explanation: IDs continue increasing after deletions; they are not reused.
Hints
- Store recipes by `recipe_id`, and keep a second map from normalized recipe name to `recipe_id`.
- For uniqueness, compare names with `casefold()` rather than the original casing.
Part 2: Search and Sort Recipes
Constraints
- 0 <= len(operations) <= 1000
- 0 <= len(ingredients), len(steps) <= 100 per recipe
- Recipe IDs should be ordered by their numeric suffix for tie-breaking
- If there are no recipes, search/list should return an empty list
Examples
Input: [['add_recipe', 'Pancakes', ['flour', 'eggs', 'milk'], ['mix', 'cook']], ['add_recipe', 'Omelette', ['eggs', 'butter'], ['whisk', 'cook']], ['add_recipe', 'FrenchToast', ['bread', 'eggs', 'milk'], ['dip', 'fry']], ['search_recipes_by_ingredient', 'EGGS'], ['list_recipes', 'name']]
Expected Output: ['recipe1', 'recipe2', 'recipe3', ['recipe2', 'recipe1', 'recipe3'], ['recipe3', 'recipe2', 'recipe1']]
Explanation: Search sorts by ingredient count, then by recipe ID. Listing by name is case-insensitive.
Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_recipe', 'soup', ['water', 'salt'], ['boil']], ['add_recipe', 'Salad', ['lettuce'], ['mix']], ['list_recipes', 'unknown'], ['search_recipes_by_ingredient', 'tomato']]
Expected Output: ['recipe1', None, 'recipe2', ['recipe2', 'recipe1'], []]
Explanation: Invalid sort key defaults to name sorting, and duplicate names are rejected.
Input: [['list_recipes', 'name'], ['search_recipes_by_ingredient', 'salt']]
Expected Output: [[], []]
Explanation: Edge case: empty manager.
Input: [['add_recipe', 'A', ['x'], ['s']], ['add_recipe', 'B', ['y'], ['s']], ['add_recipe', 'C', ['z', 'q'], ['s']], ['list_recipes', 'ingredient_count'], ['search_recipes_by_ingredient', 'Q']]
Expected Output: ['recipe1', 'recipe2', 'recipe3', ['recipe1', 'recipe2', 'recipe3'], ['recipe3']]
Explanation: Tie-breaking by recipe ID matters when ingredient counts are equal.
Hints
- Keep the full recipe data so you can sort by either name or ingredient count later.
- When sorting by recipe ID, compare the numeric part after `'recipe'`, not the raw string.
Part 3: Recipe Users and Edits
Constraints
- 0 <= len(operations) <= 1000
- 0 <= len(ingredients), len(steps) <= 100 per recipe
- User IDs are case-sensitive and must be unique exactly
- Editing a recipe to the same name with different casing is allowed if it is the same recipe
Examples
Input: [['add_recipe', 'Toast', ['bread'], ['toast']], ['add_user', 'u1'], ['edit_recipe', 'u1', 'recipe1', 'Buttered Toast', ['bread', 'butter'], ['toast', 'spread']], ['get_recipe', 'recipe1']]
Expected Output: ['recipe1', True, True, ['Buttered Toast', 'bread,butter', 'toast,spread']]
Explanation: A valid user can edit an existing recipe.
Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_user', 'chef'], ['add_user', 'chef'], ['edit_recipe', 'ghost', 'recipe1', 'Hot Soup', ['water'], ['boil']], ['edit_recipe', 'chef', 'recipe2', 'Hot Soup', ['water'], ['boil']], ['get_recipe', 'recipe1']]
Expected Output: ['recipe1', True, False, False, False, ['Soup', 'water', 'boil']]
Explanation: Duplicate users are rejected, and missing user/recipe edits fail.
Input: [['add_recipe', 'Pie', ['fruit'], ['bake']], ['add_recipe', 'Cake', ['flour'], ['bake']], ['add_user', 'u1'], ['edit_recipe', 'u1', 'recipe2', 'PIE', ['flour'], ['bake']], ['get_recipe', 'recipe2']]
Expected Output: ['recipe1', 'recipe2', True, False, ['Cake', 'flour', 'bake']]
Explanation: Editing to a conflicting name is not allowed, even with different casing.
Input: [['add_recipe', 'Tea', [], []], ['add_user', 'u'], ['edit_recipe', 'u', 'recipe1', 'tea', [], []], ['get_recipe', 'recipe1']]
Expected Output: ['recipe1', True, True, ['tea', '', '']]
Explanation: Edge case: empty ingredients/steps and editing to the same logical name with different casing.
Hints
- Use one set for users and one map from normalized recipe name to recipe ID.
- When editing, a name is only a conflict if it belongs to a different recipe.
Part 4: Recipe Version History and Rollback
Constraints
- 0 <= len(operations) <= 1000
- 0 <= len(ingredients), len(steps) <= 100 per recipe state
- Version numbers start at 1 for each recipe and increase by 1 per successful update/edit/rollback snapshot
- A recipe with no successful update/edit has no version history
Examples
Input: [['add_recipe', 'Soup', ['water'], ['boil']], ['add_user', 'chef'], ['version_recipe', 'recipe1'], ['update_recipe', 'recipe1', 'Soup', ['water', 'salt'], ['boil', 'season']], ['edit_recipe', 'chef', 'recipe1', 'Tomato Soup', ['water', 'salt', 'tomato'], ['boil', 'blend']], ['version_recipe', 'recipe1'], ['rollback_recipe', 'recipe1', 1], ['get_recipe', 'recipe1'], ['version_recipe', 'recipe1']]
Expected Output: ['recipe1', True, [], True, True, ['1:Soup:water,salt:boil,season:system', '2:Tomato Soup:water,salt,tomato:boil,blend:chef'], True, ['Soup', 'water,salt', 'boil,season'], ['1:Soup:water,salt:boil,season:system', '2:Tomato Soup:water,salt,tomato:boil,blend:chef', '3:Soup:water,salt:boil,season:rollback']]
Explanation: A recipe has no history after add, gains versions after update/edit, and rollback appends a new version.
Input: [['version_recipe', 'recipe9'], ['rollback_recipe', 'recipe1', 1], ['add_recipe', 'A', [], []], ['version_recipe', 'recipe1'], ['rollback_recipe', 'recipe1', 1], ['add_user', 'u1'], ['edit_recipe', 'ghost', 'recipe1', 'B', [], []], ['update_recipe', 'recipe2', 'C', [], []]]
Expected Output: [[], False, 'recipe1', [], False, True, False, False]
Explanation: Edge cases: missing recipe, no history yet, missing user, and invalid update target.
Input: [['add_recipe', 'One', ['a'], ['s']], ['update_recipe', 'recipe1', 'PastName', ['a'], ['s1']], ['add_recipe', 'Other', ['b'], ['t']], ['update_recipe', 'recipe1', 'CurrentName', ['a'], ['s2']], ['update_recipe', 'recipe2', 'PastName', ['b'], ['t2']], ['rollback_recipe', 'recipe1', 1], ['get_recipe', 'recipe1']]
Expected Output: ['recipe1', True, 'recipe2', True, True, False, ['CurrentName', 'a', 's2']]
Explanation: Rollback can fail if the historical name now conflicts with another current recipe.
Input: [['add_recipe', 'Tea', [], []], ['update_recipe', 'recipe1', 'Herbal Tea', [], []], ['rollback_recipe', 'recipe1', 1], ['version_recipe', 'recipe1']]
Expected Output: ['recipe1', True, True, ['1:Herbal Tea:::system', '2:Herbal Tea:::rollback']]
Explanation: Rolling back to the same current state is still valid and creates a new version entry.
Hints
- Store immutable snapshots for each recipe history; do not overwrite old versions.
- When rolling back, apply the historical state to the current recipe, then append a new history entry labeled `'rollback'`.