diff --git a/fullstack/javascript/1-variables-strings/exercises/README.md b/fullstack/javascript/1-variables-strings/exercises/README.md new file mode 100644 index 0000000..14892fb --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/README.md @@ -0,0 +1,594 @@ +# Advanced String Exercises - JavaScript Mastery + +A comprehensive exercises list that go beyond basic usage. + +## Exercise Set: Advanced String Manipulation + +### Instructions + +- Solve each exercise in vanilla JavaScript +- Use **at least 2-3 different string methods** per solution when possible +- Focus on **clean, readable code** with meaningful variable names +- Add **comments** explaining your logic +- Test with multiple inputs + +## PART 1: String Analysis & Validation (8 exercises) + +### Exercise 1: Email Validator + +**Difficulty:** Medium +**Objective:** Create a function that validates email format + +```javascript +// Requirements: +// - Must contain exactly one @ +// - Must have characters before and after @ +// - Must have a dot (.) after @ +// - Must not start or end with whitespace +// - Return true/false + +function isValidEmail(email) { + // Your code here +} + +// Test cases: +console.log(isValidEmail("user@example.com")) // true +console.log(isValidEmail("invalid.email")) // false +console.log(isValidEmail(" spaced@email.com ")) // false +console.log(isValidEmail("no@domain")) // false +``` + +### Exercise 2: Password Strength Checker + +**Difficulty:** Hard +**Objective:** Analyze password strength and return detailed feedback + +```javascript +// Requirements: +// - Check for: length (8+ chars), uppercase, lowercase, numbers, special chars +// - Return object: { strength: "weak/medium/strong", missing: [...], score: 0-5 } +// - Use multiple string methods + +function checkPasswordStrength(password) { + // Your code here +} + +// Test cases: +console.log(checkPasswordStrength("pass")) +// { strength: "weak", missing: ["length", "uppercase", "numbers", "special"], score: 1 } +console.log(checkPasswordStrength("MyP@ssw0rd")) +// { strength: "strong", missing: [], score: 5 } +``` + +### Exercise 3: Palindrome Detective + +**Difficulty:** Medium +**Objective:** Check if string is palindrome (ignore spaces, punctuation, case) + +```javascript +// Requirements: +// - Ignore spaces, punctuation, and case +// - "A man, a plan, a canal: Panama" should return true +// - Use trim, toLowerCase, replace + +function isPalindrome(str) { + // Your code here +} + +// Test cases: +console.log(isPalindrome("racecar")) // true +console.log(isPalindrome("A man, a plan, a canal: Panama")) // true +console.log(isPalindrome("hello")) // false +console.log(isPalindrome("Was it a car or a cat I saw?")) // true +``` + +### Exercise 4: Word Frequency Counter + +**Difficulty:** Medium +**Objective:** Count how many times each word appears + +```javascript +// Requirements: +// - Case insensitive +// - Ignore punctuation +// - Return object with word counts sorted by frequency +// - Use toLowerCase, trim, replace, split + +function wordFrequency(text) { + // Your code here +} + +// Test case: +console.log( + wordFrequency( + "The quick brown fox jumps over the lazy dog. The dog was really lazy." + ) +) +// { the: 2, lazy: 2, dog: 2, quick: 1, brown: 1, ... } +``` + +### Exercise 5: Username Sanitizer + +**Difficulty:** Easy-Medium +**Objective:** Clean and validate username input + +```javascript +// Requirements: +// - Remove whitespace from start/end +// - Replace spaces with underscores +// - Convert to lowercase +// - Remove special characters (keep only letters, numbers, underscore) +// - Ensure length is 3-20 characters +// - Return sanitized username or null if invalid + +function sanitizeUsername(input) { + // Your code here +} + +// Test cases: +console.log(sanitizeUsername(" John Doe!@# ")) // "john_doe" +console.log(sanitizeUsername("ab")) // null (too short) +console.log(sanitizeUsername("User Name 123")) // "user_name_123" +``` + +### Exercise 6: Credit Card Masker + +**Difficulty:** Easy +**Objective:** Mask credit card number showing only last 4 digits + +```javascript +// Requirements: +// - Accept string of 16 digits +// - Replace first 12 digits with * +// - Keep spaces if present +// - "1234 5678 9012 3456" becomes "**** **** **** 3456" + +function maskCreditCard(cardNumber) { + // Your code here +} + +// Test cases: +console.log(maskCreditCard("1234567890123456")) // "************3456" +console.log(maskCreditCard("1234 5678 9012 3456")) // "**** **** **** 3456" +``` + +### Exercise 7: Initials Extractor + +**Difficulty:** Easy +**Objective:** Extract initials from full name + +```javascript +// Requirements: +// - Handle multiple middle names +// - Return uppercase initials with dots +// - "john paul jones" → "J.P.J." +// - Handle extra spaces + +function getInitials(fullName) { + // Your code here +} + +// Test cases: +console.log(getInitials("John Doe")) // "J.D." +console.log(getInitials("Mary Jane Watson")) // "M.J.W." +console.log(getInitials(" josé maría lópez ")) // "J.M.L." +``` + +### Exercise 8: Sentence Case Converter + +**Difficulty:** Medium +**Objective:** Convert any text to proper sentence case + +```javascript +// Requirements: +// - First letter uppercase, rest lowercase +// - After every period, question mark, exclamation: capitalize +// - Preserve single spaces between words +// - "hELLo. hOW ARE you?" → "Hello. How are you?" + +function toSentenceCase(text) { + // Your code here +} + +// Test cases: +console.log(toSentenceCase("hELLo WoRLD. hOW aRe YOU?")) +// "Hello world. How are you?" +console.log(toSentenceCase("tHIS is A test! iS it WORking?")) +// "This is a test! Is it working?" +``` + +--- + +## PART 2: String Transformation (7 exercises) + +### Exercise 9: Slug Generator + +**Difficulty:** Medium +**Objective:** Convert blog post title to URL-friendly slug + +```javascript +// Requirements: +// - Convert to lowercase +// - Replace spaces with hyphens +// - Remove special characters except hyphens +// - Remove consecutive hyphens +// - Trim hyphens from start/end +// - "Hello World! How Are You?" → "hello-world-how-are-you" + +function generateSlug(title) { + // Your code here +} + +// Test cases: +console.log(generateSlug("My First Blog Post!")) +// "my-first-blog-post" +console.log(generateSlug(" JavaScript Tips & Tricks ")) +// "javascript-tips-tricks" +``` + +### Exercise 10: Camel Case Converter + +**Difficulty:** Medium +**Objective:** Convert string to camelCase, PascalCase, snake_case, kebab-case + +```javascript +// Requirements: +// - One function with 'type' parameter: 'camel', 'pascal', 'snake', 'kebab' +// - Handle multiple input formats +// - "hello world" → "helloWorld" (camel) +// - "hello-world" → "HelloWorld" (pascal) + +function convertCase(str, type) { + // Your code here +} + +// Test cases: +console.log(convertCase("hello world", "camel")) // "helloWorld" +console.log(convertCase("hello world", "pascal")) // "HelloWorld" +console.log(convertCase("hello world", "snake")) // "hello_world" +console.log(convertCase("hello world", "kebab")) // "hello-world" +console.log(convertCase("hello-world-test", "camel")) // "helloWorldTest" +``` + +### Exercise 11: Text Abbreviator + +**Difficulty:** Hard +**Objective:** Shorten text intelligently to specific length + +```javascript +// Requirements: +// - If text > maxLength, truncate at word boundary (don't cut words) +// - Add "..." at end +// - Preserve at least one word even if it exceeds maxLength +// - Remove trailing punctuation before "..." + +function abbreviate(text, maxLength) { + // Your code here +} + +// Test cases: +console.log(abbreviate("The quick brown fox jumps over the lazy dog", 20)) +// "The quick brown fox..." +console.log(abbreviate("Hello", 10)) // "Hello" +console.log( + abbreviate("This is a very long sentence that needs truncation", 25) +) +// "This is a very long..." +``` + +### Exercise 12: Phone Number Formatter + +**Difficulty:** Medium +**Objective:** Format phone numbers to standard format + +```javascript +// Requirements: +// - Accept 10 digits in any format +// - Output: (XXX) XXX-XXXX +// - Remove all non-digit characters first +// - Return null if not exactly 10 digits + +function formatPhoneNumber(phone) { + // Your code here +} + +// Test cases: +console.log(formatPhoneNumber("1234567890")) // "(123) 456-7890" +console.log(formatPhoneNumber("123-456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("(123) 456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("12345")) // null +``` + +### Exercise 13: Title Case Converter (Advanced) + +**Difficulty:** Hard +**Objective:** Convert to title case with English grammar rules + +```javascript +// Requirements: +// - Capitalize first and last word always +// - Capitalize all words except: a, an, the, and, but, or, for, nor, on, at, to, by, in +// - Articles after colon (:) should be capitalized +// - "the quick brown fox" → "The Quick Brown Fox" +// - "a tale of two cities" → "A Tale of Two Cities" + +function toTitleCase(text) { + // Your code here +} + +// Test cases: +console.log(toTitleCase("the lord of the rings")) +// "The Lord of the Rings" +console.log(toTitleCase("a tale of two cities")) +// "A Tale of Two Cities" +console.log(toTitleCase("javascript: the good parts")) +// "JavaScript: The Good Parts" +``` + +### Exercise 14: Reverse Words (Keep Order) + +**Difficulty:** Easy +**Objective:** Reverse each word but keep word order + +```javascript +// Requirements: +// - "Hello World" → "olleH dlroW" +// - Preserve spacing +// - Use split, reverse (array method), join + +function reverseWords(sentence) { + // Your code here +} + +// Test cases: +console.log(reverseWords("Hello World")) // "olleH dlroW" +console.log(reverseWords("JavaScript is awesome")) // "tpircSavaJ si emosewa" +``` + +### Exercise 15: Character Replacer + +**Difficulty:** Medium +**Objective:** Replace characters based on a mapping object + +```javascript +// Requirements: +// - Accept string and object mapping +// - Replace all occurrences +// - Case sensitive +// - { 'a': '@', 'e': '3', 'i': '!', 'o': '0' } +// - "hello" → "h3ll0" + +function replaceChars(str, mapping) { + // Your code here +} + +// Test cases: +const leetSpeak = { a: "@", e: "3", i: "!", o: "0", s: "$" } +console.log(replaceChars("hello world", leetSpeak)) // "h3ll0 w0rld" +console.log(replaceChars("awesome", leetSpeak)) // "@w3$0m3" +``` + +--- + +## PART 3: String Search & Extract (5 exercises) + +### Exercise 16: URL Parameter Extractor + +**Difficulty:** Hard +**Objective:** Extract query parameters from URL + +```javascript +// Requirements: +// - Parse URL query string into object +// - Handle multiple parameters +// - Handle parameters without values +// - "?name=john&age=30&active" → { name: "john", age: "30", active: "" } + +function extractParams(url) { + // Your code here +} + +// Test cases: +console.log(extractParams("https://example.com/page?name=john&age=30")) +// { name: "john", age: "30" } +console.log( + extractParams("https://example.com/search?q=javascript&lang=en&sort") +) +// { q: "javascript", lang: "en", sort: "" } +``` + +### Exercise 17: Hashtag Extractor + +**Difficulty:** Medium +**Objective:** Find all hashtags in text + +```javascript +// Requirements: +// - Find all words starting with # +// - Return array of hashtags (without #) +// - No duplicates +// - Case insensitive comparison +// - "#JavaScript is awesome #javascript #WebDev" → ["javascript", "webdev"] + +function extractHashtags(text) { + // Your code here +} + +// Test cases: +console.log(extractHashtags("Love #JavaScript and #WebDev #javascript")) +// ["javascript", "webdev"] +console.log(extractHashtags("No hashtags here")) +// [] +``` + +### Exercise 18: Email Extractor + +**Difficulty:** Medium +**Objective:** Find all email addresses in text + +```javascript +// Requirements: +// - Extract all valid-looking emails +// - Return array +// - Basic validation: word@word.word pattern + +function extractEmails(text) { + // Your code here +} + +// Test cases: +console.log( + extractEmails("Contact us at support@example.com or sales@example.com") +) +// ["support@example.com", "sales@example.com"] +``` + +### Exercise 19: Find Longest Word + +**Difficulty:** Easy +**Objective:** Return longest word and its length + +```javascript +// Requirements: +// - Ignore punctuation +// - Return object: { word: "longest", length: 7 } +// - If tie, return first occurrence + +function findLongestWord(sentence) { + // Your code here +} + +// Test cases: +console.log(findLongestWord("The quick brown fox jumps")) +// { word: "quick", length: 5 } or { word: "brown", length: 5 } or { word: "jumps", length: 5 } +console.log(findLongestWord("JavaScript is amazing!")) +// { word: "JavaScript", length: 10 } +``` + +### Exercise 20: Text Between Delimiters + +**Difficulty:** Medium +**Objective:** Extract text between two markers + +```javascript +// Requirements: +// - Extract all text between start and end markers +// - Return array +// - "Hello [name], welcome to [place]" with markers "[" and "]" +// → ["name", "place"] + +function extractBetween(text, start, end) { + // Your code here +} + +// Test cases: +console.log(extractBetween("Hello [name], welcome to [place]", "[", "]")) +// ["name", "place"] +console.log(extractBetween("The {quick} brown {fox}", "{", "}")) +// ["quick", "fox"] +``` + +--- + +## Submission Instructions + +**When you complete the exercises:** + +1. **Create a file:** `string-exercises.js` +2. **Include all 20 solutions** with your test cases +3. **Add comments** explaining your approach +4. **Test each function** with the provided test cases plus your own +5. **Share the complete file** for review + +**I will review:** + +- ✓ Correctness (does it work?) +- ✓ Code quality (readable, maintainable?) +- ✓ Method usage (using string methods effectively?) +- ✓ Edge case handling +- ✓ Professional coding style + +--- + +## After Exercises: Advanced String Exam + +**Once you complete and submit these exercises, I will provide:** + +1. **A 10-question written exam** testing string concepts +2. **3 coding challenges** (medium-hard difficulty) +3. **1 real-world scenario problem** (simulating actual job task) + +**Exam will cover:** + +- String immutability +- Method chaining +- Performance considerations +- Unicode and encoding +- Common pitfalls + +--- + +## Time Estimate + +**Realistic completion time:** + +- Easy exercises (7): 15-30 min each = 2-3.5 hours +- Medium exercises (10): 30-45 min each = 5-7.5 hours +- Hard exercises (3): 45-60 min each = 2-3 hours + +## Tips for Success + +1. Read requirements carefully + + - List what methods you'll need before coding + - Plan your approach + +2. Test incrementally + +````javascript +// Don't write entire function then test +// Test each step + +function example(str) { + console.log("Input:", str) // Test 1 + const trimmed = str.trim() + console.log("Trimmed:", trimmed) // Test 2 + // Continue... +} + +3. Use descriptive variable names + +```javascript +// Bad +const x = str.split(" ") + +// Good +const words = str.split(" ")``` + +4. Comment your logic + +```javascript +// Check if email contains exactly one @ symbol +const atCount = email.split("@").length - 1 +if (atCount !== 1) return false +``` + +5. Handle edge cases + +- Empty strings +- Only whitespace +- Special characters +- Very long strings +- Null/undefined (if applicable) + +--- + +## Ready to Start? + +When you've completed all 20 exercises, share your `string-exercises.js` file and I'll: + +1. Review each solution (0-100 score per exercise) +2. Provide detailed feedback +3. Suggest optimizations +4. Then give you the final exam +```` diff --git a/fullstack/javascript/1-variables-strings/exercises/script-claude-ai.js b/fullstack/javascript/1-variables-strings/exercises/script-claude-ai.js new file mode 100644 index 0000000..08b739e --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/script-claude-ai.js @@ -0,0 +1,1148 @@ +/* +String Exercises - JavaScript Mastery +Author: Yoandy Doble Herrera +Date: 11/01/2026 +freeCodeCamp String Quiz Score: 20/20 (100%) +*/ + +/* +Exercise 1: Email Validator +Difficulty: Medium +Requirements: +[x] - Must contain exactly one @ +[x] - Must have characters before and after @ +[x] - Must have a dot (.) after @ +[x] - Must not start or end with whitespace +[x] - Return true/false +*/ + +//Exercise 1 - Email Validator SOLVED CLAUDE +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmailThree(email) { + // check for whitespaces + if (email !== email.trim()) { + return false + } + // Split by @ - must be exactly 2 parts + const parts = email.split("@") + if (parts !== 2) { + return false + } + const [local, domain] = parts + + // Both parts must exist and domain must have a dot + return local.length > 0 && domain.length > 0 && domain.includes(".") +} + +//Exercise 1 - Email Validator SOLVED CLAUDE +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmailFour(email) { + if (email !== email.trim()) return false + return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(email) +} + +// Test cases: +console.log(`Exercise 1: Email Validator`) +console.log(isValidEmailThree("user@.")) // false +console.log(isValidEmailThree(" invalid.email")) // false +console.log(isValidEmailThree(" spaced@email.com ")) // false +console.log(isValidEmailThree("no@domain ")) // false +console.log(isValidEmailThree("code.claude@domain.fr @ ca")) //false +console.log(isValidEmailThree("yoandy.doble@gmail.com")) //true +console.log("\n==================================\n") + +/* +Exercise 5: Username Sanitizer || Claude AI SOLVED +Difficulty:** Easy-Medium +Requirements: +[x] - Remove whitespace from start/end +[x] - Replace spaces with underscores +[x] - Convert to lowercase +[x] - Remove special characters (keep only letters, numbers, underscore) +[x] - Ensure length is 3-20 characters +[x] - Return sanitized username or null if invalid +*/ + +/** + * Function that sanitize spaces. + * @param {String} userName userName input to remove double spaces. + * @returns {String} username with spaces. + */ +function sanitizeUsernameAI(input) { + // Remove whitespace, lowercase + const cleaned = input.trim().toLowerCase() + + // Remove special chars (keep letters, numbers, spaces) + const alphanumeric = cleaned.replace(/[^a-z0-9\s]/g, "") + + // Replace spaces with underscores, remove consecutive underscores + const username = alphanumeric.replace(/\s+/g, "_") + + // Check length + return username.length >= 3 && username.length <= 20 ? username : null +} + +// Test cases: +console.log(`Exercise 5: Username Sanitizer AI`) +console.log(sanitizeUsernameAI(" John Doe!@# ")) // "john_doe" +console.log(sanitizeUsernameAI("ab")) // null (too short) +console.log(sanitizeUsernameAI("User Name 123")) // "user_name_123" +console.log(sanitizeUsernameAI(" jane_doe@2025")) // "user_name_123" +console.log(sanitizeUsernameAI("M ike_ Lucy@home ")) // "mike_lucy_home" +console.log(sanitizeUsernameAI(" emily&tom @home!")) // "emily_tom_home" +console.log(sanitizeUsernameAI(" David @team#2025")) // "david_team_2025" +console.log(sanitizeUsernameAI(" Sarah-2026!")) // sarah_2026 +console.log(sanitizeUsernameAI("longterm-androidstudio-Iphone.Visual-Studio-Code_All In One")) // null (too long) +console.log("\n==================================\n") + +/* +Exercise 6: Credit Card Masker +**Difficulty:** Easy +**Objective:** Mask credit card number showing only last 4 digits +Requirements: +[x] - Accept string of 16 digits +[x] - Replace first 12 digits with * +[x] - Keep spaces if present +[x] - "1234 5678 9012 3456" becomes "**** **** **** 3456" +*/ + +/** + * Function that receive a credit card number and apply a mask showing only last 4 digits. + * @param {String} cardNumber credit card number with 16 or 19 digits. + * @returns credit card number showing only last 4 digits. + */ +function maskCreditCard(cardNumber) { + // Remove spaces, check length + const cleaned = cardNumber.replace(/\D/g, "") + if (cleaned.length !== 16) { + return "Invalid credit card number" + } + // Mask first 12 digits + const masked = "*".repeat(12) + cleaned.slice(-4) + // Re-add spaces if original had them + if (cardNumber.includes(" ")) { + return masked.match(/.{1,4}/g).join(" ") + } + return masked +} + +// Test cases: +console.log(`Exercise 6: Credit Card Masker`) +console.log(maskCreditCard("1234567899876549")) // "************3456" +console.log(maskCreditCard("1234 5678 9012 3456")) // "**** **** **** 3456" +// Valid cases +console.log(maskCreditCard("1234567890123456")) // "************3456" +console.log(maskCreditCard("1234 9632 9012 3456")) // "**** **** **** 3456" + +// Invalid cases +console.log(maskCreditCard("4532-1488-0343-6467")) // "Wrong credit card number" +console.log(maskCreditCard("123")) // "Wrong credit card number" +console.log(maskCreditCard("abcd1234efgh5678")) // "Wrong credit card number" +console.log(maskCreditCard("")) // "" (string vacío)*/ +console.log("\n==================================\n") + +/* +Exercise 7: Initials Extractor +**Difficulty:** Easy +**Objective:** Extract initials from full name +Requirements: +[x] - Handle multiple middle names +[x] - Return uppercase initials with dots +[x] - "john paul jones" → "J.P.J." +[x] - Handle extra spaces +*/ + +/** + * Function that extract initials from full name. + * @param {String} fullName name and last name or name, middle name and last name. + * @returns {String} Return uppercase initials from full name with dots. + */ +function getInitials(fullName) { + return ( + fullName + .trim() + .split(/\s+/) // Split by one or more spaces + .filter((word) => word.length > 0) // Remove empty strings + .map((word) => word[0].toUpperCase()) + .join(".") + "." + ) +} + +// Test cases: +console.log(`Exercise 7: Initials Extractor`) +console.log(getInitials("John Doe")) // "J.D." +console.log(getInitials("Mary Jane Watson")) // "M.J.W." +console.log(getInitials("Yoandy Doble Herrera")) // "Y.D.H." +console.log(getInitials(" Arthur Conan ")) // "A.C." +console.log(getInitials(" josé maría lópez ")) // "J.M.L." +console.log(getInitials(" Leonel Messi Cuchitiny ")) // "L.M.C." +console.log("\n==================================\n") + +/* +Exercise 3: Palindrome Detective +Difficulty: Medium +Objective: Check if string is palindrome (ignore spaces, punctuation, case) +Requirements: +[x] - Ignore spaces, punctuation, and case +[x] - "A man, a plan, a canal: Panama" should return true +[x] - Use trim, toLowerCase, replace +*/ + +function isPalindrome(str) { + // Use trim, toLowerCase + const strLowerCase = str.trim().toLowerCase() + const strSanitized = strLowerCase.replace(/\W/g, "") + console.log("Sanitized Word:\n" + strSanitized) + return strSanitized === reverseStr(strSanitized) ? true : false +} + +// Use built-in methods +/** + * Reverse string. + * @param {String} str any string. + * @returns return string reversed. + */ +function reverseStr(str) { + return str.split("").reverse().join("") +} +// Test cases: +console.log(`Exercise 3: Palindrome Detective`) +console.log(isPalindrome("racecar}")) // true +console.log(isPalindrome("A man, a plan, a canal: Panama")) // true +console.log(isPalindrome("hello")) // false +console.log(isPalindrome("Was it a car or a cat I saw?")) // true +console.log(isPalindrome("-Javascript-")) // false +console.log(isPalindrome("Wat is your name?")) // false +console.log("\n==================================\n") + +/* +Exercise 4: Word Frequency Counter +Difficulty: Medium +Objective: Count how many times each word appears +Requirements: +[x] - Case insensitive +[x] - Ignore punctuation +[x] - Return object with word counts sorted by frequency +[x] - Use toLowerCase, trim, replace, split +*/ + +/** + * Function that count how many times each word appears. + * @param {String} text any sentence. + * @returns {Object} object with word counts sorted by frequency. + */ +function wordFrequency(text) { + // Step 1: Use toLowerCase, trim, replace, split + const sentenceArray = text + .trim() + .toLowerCase() + .replace(/[^\w\s]/g, "") + .split(" ") + // Step 2: Count frequencies + const wordCounts = {} + for (const word of sentenceArray) { + wordCounts[word] = (wordCounts[word] || 0) + 1 + } + // Step 3: array descendant sort + const ordered = Object.entries(wordCounts).sort(([, a], [, b]) => b - a) + // convert array to object again + return Object.fromEntries(ordered) +} + +// Test case: +console.log(`Exercise 4: Word Frequency Counter`) +console.log(wordFrequency("The quick brown fox jumps over the lazy dog. The dog was really lazy.")) +console.log(wordFrequency("I love Claude AI. I enjoy coding with him. Claude AI is amazing.")) + +// { the: 2, lazy: 2, dog: 2, quick: 1, brown: 1, ... } +console.log("\n==================================\n") + +/* +Exercise 9: Slug Generator +Difficulty: Medium +Objective: Convert blog post title to URL-friendly slug +Requirements: +[x] - Convert to lowercase +[x] - Replace spaces with hyphens +[x] - Remove special characters except hyphens +[x] - Remove consecutive hyphens +[x] - Trim hyphens from start/end +[x] - "Hello World! How Are You?" → "hello-world-how-are-you" +*/ + +/** + * Function that convert blog post title to URL-friendly slug. + * @param {String} title Blog post title. + * @returns {String} URL-friendly slug. + */ +function generateSlug(title) { + return title + .trim() + .toLowerCase() + .replace(/[^\w\s-]/g, "") // Keep only letters, numbers, spaces, hyphens + .replace(/\s+/g, "-") // Replace spaces with hyphens + .replace(/-+/g, "-") // Remove consecutive hyphens + .replace(/^-|-$/g, "") // Remove leading/trailing hyphens +} + +// Test cases: +console.log(`Exercise 9: Slug Generator`) +console.log(generateSlug("My First Blog Post!")) +// "my-first-blog-post" +console.log(generateSlug(" JavaScript Tips & Tricks ")) +// "javascript-tips-tricks" +console.log(generateSlug(" codebydoble @ qvapay .me ")) +// "codebydoble-qvapay-me" +console.log(generateSlug(" url-page.com ")) +// "codebydoble-qvapay-me" +console.log("\n==================================\n") + +/* +Exercise 14: Reverse Words (Keep Order) +Difficulty: Easy +Objective: Reverse each word but keep word order +Requirements: +[x] - "Hello World" → "olleH dlroW" +[x] - Preserve spacing +[x] - Use split, reverse (array method), join +*/ + +/** + * Function that reverse each word but keep word order and preserve spacing. + * @param {String} sentence a word or paragraph. + * @returns a reversed sentence. + */ +function reverseWords(sentence) { + return sentence + .split(" ") + .map((word) => word.split("").reverse().join("")) + .join(" ") +} + +// Test cases: +console.log(`Exercise 14: Reverse Words (Keep Order)`) +console.log(reverseWords("")) +console.log(reverseWords(" Barcelona")) +console.log(reverseWords("Hello World")) // "olleH dlroW" +console.log(reverseWords("JavaScript is awesome")) // "tpircSavaJ si emosewa" +console.log(reverseWords("JavaScript is awesome")) // "tpircSavaJ si emosewa" +console.log("\n==================================\n") + +/* +Exercise 2: Password Strength Checker +Difficulty: Hard +Objective: Analyze password strength and return detailed feedback +Requirements: +[x] - Check for: length (8+ chars), uppercase, lowercase, numbers, special chars +[x] - Return object: { strength: "weak/medium/strong", missing: [...], score: 0-5 } +[x] - Use multiple string methods +*/ + +/** + * Function that analyze password strength. Check for: length (8+ chars), uppercase, lowercase, numbers and special chars. + * @param {String} password string password to check. + * @returns {Object} return an object with detailed feedback about password. + */ +function checkPasswordStrength(password) { + const checks = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + numbers: /[0-9]/.test(password), + special: /[^a-zA-Z0-9]/.test(password), + } + + const score = Object.values(checks).filter(Boolean).length + const missing = Object.keys(checks).filter((key) => !checks[key]) + + let strength + if (score <= 2) strength = "weak" + else if (score <= 4) strength = "medium" + else strength = "strong" + + return { strength, missing, score } +} + +// Test cases: +console.log(`Exercise 2: Password Strength Checker`) +console.log(checkPasswordStrength("pa_ss")) +// { strength: "weak", missing: ["length", "uppercase", "numbers", "special"], score: 1 } +console.log(checkPasswordStrength("fund1*")) +// { strength: "medium", missing: ["length", "uppercase"], score: 3 } +console.log(checkPasswordStrength("MyP@ssw0rd")) +// { strength: "strong", missing: [], score: 5 } +console.log("\n==================================\n") + +/* +Exercise 8: Sentence Case Converter +Difficulty: Medium +Objective: Convert any text to proper sentence case +Requirements: +[x] - First letter uppercase, rest lowercase +[x] - After every period, question mark, exclamation: capitalize +[x] - Preserve single spaces between words +[x] - "hELLo. hOW ARE you?" → "Hello. How are you?" +[x] - " hELLo! → "Hello!" trim, lowerCase and after upperCase in first letter +[x] - " hELLo? → "Hello?" trim, lowerCase and after upperCase in first letter +[x] - " hELLo. → "Hello." trim, lowerCase and after upperCase in first letter +*/ + +/** + * Function that convert any text to proper sentence case. + * @param {String} text any text. + * @returns {String} A proper sentence case. + */ +function toSentenceCase(text) { + const cleaned = text.trim().toLowerCase() + + // Split by punctuation while keeping the punctuation + const sentences = cleaned.split(/([.!?]\s+)/) + // "hello. world! test?" + // → ["hello", ". ", "world", "! ", "test", "?"] + + const processed = sentences.map((part) => { + // Punctuation parts: return as-is + if (/^[.!?]\s+$/.test(part)) return part + + // Sentence parts: capitalize first letter + if (part.length > 0) { + return part.charAt(0).toUpperCase() + part.slice(1) + } + + return part + }) + + return processed.join("") +} + +// Test cases: +console.log(`Exercise 8: Sentence Case Converter`) +console.log(toSentenceCase(" hELLo! ")) +console.log(toSentenceCase(" hELLo? ")) +console.log(toSentenceCase(" hELLo. ")) +console.log(toSentenceCase("hELLo WoRLD. hOW aRe YOU? I'm fine! Bye.")) +console.log(toSentenceCase("tHIS is A test! iS it WORking?")) +console.log( + toSentenceCase( + "strings are IMMutable primitives - any operation that appears to modify a string actually creates a new one, leaving the original intact. tHIS ensures data safety? but requires awareness for performance optimization.", + ), +) +console.log("\n==================================\n") + +/* +Exercise 10: Camel Case Converter +Difficulty: Medium +Objective: Convert string to camelCase, PascalCase, snake_case, kebab-case +*/ +// Requirements: +// [x] - One function with 'type' parameter: 'camel', 'pascal', 'snake', 'kebab' +// [x] - Handle multiple input formats +// [x] - "hello world" → "helloWorld" (camel) +// [x] - "hello-world" → "HelloWorld" (pascal) + +/** + * Function that convert string to camelCase, PascalCase, snake_case and kebab-case. + * @param {String} str any sentence. + * @param {String} type parameter: 'camel', 'pascal', 'snake', 'kebab' + * @returns {String} return string converted to type. + */ +function convertCase(str, type) { + const sentence = skipDoubleSpace(str.replace(/[-_.,!?;:*+\\[\]${}()@'"|/$]/g, " ")).split(" ") + switch (type) { + case "camel": + return camelCase(sentence) + case "pascal": + return pascalCase(sentence) + case "snake": + return snakeCase(sentence) + default: + return kebabCase(sentence) + } +} + +/** + * Function that remove double spaces from string. + * @param {String} sentence any single word. + * @returns {String} string without double spaces. + */ +function skipDoubleSpace(sentence) { + return sentence.split(/\s+/g).join(" ") +} + +/** + * Function that convert string to camelCase. + * @param {Array} str any sentence array in lowercase. + * @returns {String} return string converted to camelCase. + */ +const camelCase = (str) => { + //camelCase: "helloWorldTest" + if (str.length === 1) return str.join("") + return str + .map((word, index) => { + if (index !== 0) { + return capitalize(word) + } + return word + }) + .join("") +} + +/** + * Function that convert string to PascalCase. + * @param {Array} str any sentence array. + * @returns {String} return string converted to PascalCase. + */ +const pascalCase = (str) => { + //PascalCase: "HelloWorld" + if (str.length === 1) return capitalize(str[0]) + return str + .map((word) => { + return capitalize(word) + }) + .join("") +} + +/** + * Function that convert string to snake_case. + * @param {Array} str any sentence array. + * @returns {String} return string converted to snake_case. + */ +const snakeCase = (str) => { + //snake_case: "hello_world" + return str.length === 1 ? str.join("") : str.join("_") +} + +/** + * Function that convert string to kebab-case. + * @param {Array} str any sentence array. + * @returns {String} return string converted to kebab-case. + */ +const kebabCase = (str) => { + //kebab-case: "hello-world" + return str.length === 1 ? str.join("") : str.join("-") +} + +// Test cases: +console.log(`Exercise 10: Camel Case Converter`) +console.log(convertCase("code", "pascal")) // "helloWorld" +console.log(convertCase("hello world", "camel")) // "helloWorld" +console.log(convertCase("hello world", "pascal")) // "HelloWorld" +console.log(convertCase("hello world", "snake")) // "hello_world" +console.log(convertCase("hello- world-test", "snake")) // "helloWorldTest" +console.log(convertCase("hello wor ld", "kebab")) // "hello-world" +console.log(convertCase("hello- world-test", "camel")) // "helloWorldTest" +console.log("\n==================================\n") +/* + +Exercise 11: Text Abbreviator +Difficulty: Hard +Objective: Shorten text intelligently to specific length +*/ +// Requirements: +// [x] - If text > maxLength, truncate at word boundary (don't cut words) +// [x] - Add "..." at end +// [x] - Preserve at least one word even if it exceeds maxLength +// [x] - Remove trailing punctuation before "..." + +/** + * Function that shorten text intelligently to specific length. + * @param {String} text any sentence. + * @param {Number} maxLength specific length to shorten text. + */ +function abbreviate(text, maxLength) { + // Return original if short enough + if (text.length <= maxLength) { + return text + } + // Get substring up to maxLength + const substring = text.slice(0, maxLength) + // Find last space (word boundary) + const lastSpace = substring.lastIndexOf(" ") + // Handle edge case: no spaces found + + if (lastSpace === -1) { + // Check if entire text has no spaces + if (!text.includes(" ")) { + // Single word longer than maxLength - return it all + return removeTrailingAtEnd(text) + "..." + } + // First word exceeds maxLength - preserve it + const firstSpace = text.indexOf(" ", maxLength) + return removeTrailingAtEnd(text.slice(0, firstSpace)) + "..." + } + + // Truncate at last word boundary + return removeTrailingAtEnd(substring.slice(0, lastSpace)) + "..." +} + +/** + * Function that remove trailing punctuation in strings. + * @param {String} word any string word. + * @returns {String} words without trailing punctuation. + */ +const removeTrailingAtEnd = (word) => word.replace(/[.,!?;:\-]+$/, "").trim() + +// Test cases: +console.log(`Exercise 11: Text Abbreviator`) +console.log(abbreviate("The quick brown fox...) jumps over the lazy dog", 21)) +// "The quick brown fox..." +console.log(abbreviate("Hello", 10)) // "Hello" +console.log(abbreviate("This is a very, long! sentence. That needs truncation", 27)) +// "This is a very long sentence..." +console.log( + abbreviate( + "Trailing punctuation refers to punctuation marks (like commas, periods, semicolons, colons, etc.) that appear at the end of a word, phrase, or sentence.", + 93, + ), +) +console.log("\n==================================\n") +/* +Exercise 12: Phone Number Formatter +Difficulty: Medium +Objective: Format phone numbers to standard format +*/ +// Requirements: +// [x] - Accept 10 digits in any format +// [x] - Output: (XXX) XXX-XXXX +// [x] - Remove all non-digit characters first +// [x] - Return null if not exactly 10 digits + +/** + * Formats a phone number to standard US format: (XXX) XXX-XXXX + * @param {string} phone - Phone number in any format + * @returns {string|null} Formatted phone number or null if invalid + */ +function formatPhoneNumber(phone) { + // Extract only digits using RegExp + const digits = phone.replace(/\D/g, "") + + // Validate exactly 10 digits + if (digits.length !== 10) { + return null + } + + // Format using template literal and slice + return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}` +} +// Test cases: +console.log(`Exercise 12: Phone Number Formatter`) +console.log(formatPhoneNumber("1234567890")) // "(123) 456-7890" +console.log(formatPhoneNumber("123-456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("(123) 456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("12345")) // null +console.log(formatPhoneNumber("{123}456}789 0 3365")) // null +console.log("\n==================================\n") + +/* +Exercise 13: Title Case Converter (Advanced) +Difficulty: Hard +Objective: Convert to title case with English grammar rules +*/ +// Requirements: +// [x] - Capitalize first and last word always +// [x] - Capitalize all words except: a, an, the, and, but, or, for, nor, on, at, to, by, in +// [x] - Articles after colon (:) should be capitalized +// [x] - "the quick brown fox" → "The Quick Brown Fox" +// [x] - "a tale of two cities" → "A Tale of Two Cities" + +/** + * Converts text to title case following English grammar rules + * @param {string} text - Text to convert + * @returns {string} Title-cased text + */ +function toTitleCase(text) { + // Words that should remain lowercase (unless first/last/after colon) + const exceptions = new Set(["a", "an", "the", "and", "but", "or", "for", "nor", "on", "at", "to", "by", "in", "of"]) + + const words = text.trim().toLowerCase().split(/\s+/) + + return words + .map((word, index) => { + // Always capitalize first and last word + if (index === 0 || index === words.length - 1) { + return capitalize(word) + } + + // Check if previous word ended with colon + const prevWord = words[index - 1] + if (prevWord && prevWord.endsWith(":")) { + return capitalize(word) + } + + // Capitalize unless it's an exception word + return exceptions.has(word) ? word : capitalize(word) + }) + .join(" ") +} + +/** + * Capitalizes first letter of a word + * @param {string} word - Word to capitalize + * @returns {string} Capitalized word + */ +function capitalize(word) { + return word.charAt(0).toUpperCase() + word.slice(1) +} +// Test cases: +console.log(`Exercise 13: Title Case Converter (Advanced)`) +console.log(toTitleCase("the lord of the rings")) +// "The Lord of the Rings" +console.log(toTitleCase("a tale of two cities")) +// "A Tale of Two Cities" +console.log(toTitleCase("thE an THE")) +// "The an The" +console.log(toTitleCase("javascript: the gOOd parts")) +// "JavaScript: The Good Parts" +console.log(toTitleCase(" javascript ")) +// "Javascript" +console.log(toTitleCase(" Array sentence: just finished an amazing dayx. ")) +console.log("\n==================================\n") + +/* +Exercise 15: Character Replacer +Difficulty: Medium +Objective: Replace characters based on a mapping object +*/ +// Requirements: +// [x] - Accept string and object mapping +// [x] - Replace all occurrences +// [x] - Case sensitive +// [x] - { 'a': '@', 'e': '3', 'i': '!', 'o': '0' } +// [x] - "hello" → "h3ll0" + +/** + * Replaces characters in string based on mapping object + * @param {string} str - String to process + * @param {Object} mapping - Character mapping {old: new} + * @returns {string} String with replacements applied + */ +function replaceChars(str, mapping) { + return str + .split("") + .map((char) => mapping[char] || char) + .join("") +} + +// Alternative: Your approach (also excellent) +function replaceCharsYourWay(str, mapping) { + const result = [] + for (let i = 0; i < str.length; i++) { + if (mapping[str[i]] !== undefined) { + result.push(mapping[str[i]]) + } else { + result.push(str[i]) + } + } + return result.join("") +} +// Test cases: +console.log(`Exercise 15: Character Replacer`) +const leetSpeak = { a: "@", e: "3", i: "!", o: "0", s: "$" } +const mappingSpeak = { a: "4", e: "3", i: "1", o: "0", s: "5", u: "v" } +console.log(replaceChars("hello world", leetSpeak)) // "h3ll0 w0rld" +console.log(replaceChars("awesome", leetSpeak)) // "@w3$0m3" +console.log(replaceChars("Welcome to the paradise", mappingSpeak)) // "W3lc0m3 t0 th3 p4r4d153" +console.log(replaceChars("Javascript is awesome. Happy[] coding. Have fun.", mappingSpeak)) // "J4v45cr1pt 15 4w350m3. H4ppy c0d1ng. H4v3 fvn." +console.log("\n==================================\n") + +/* +Exercise 16: URL Parameter Extractor +Difficulty: Hard +Objective: Extract query parameters from URL +*/ +// Requirements: +// [x] - Parse URL query string into object +// [x] - Handle multiple parameters +// [x] - Handle parameters without values +// [x] - "?name=john&age=30&active" → { name: "john", age: "30", active: "" } + +/** + * Extracts query parameters from URL into object + * @param {string} url - URL with query parameters + * @returns {Object} Parameters as key-value pairs + */ +function extractParams(url) { + // Find query string (after ?) + const queryIndex = url.indexOf("?") + + // No parameters + if (queryIndex === -1) { + return {} + } + + const queryString = url.slice(queryIndex + 1) + const params = {} + + // Split by & to get individual parameters + queryString.split("&").forEach((param) => { + const [key, value = ""] = param.split("=") + params[key] = value + }) + + return params +} + +// Alternative: Using URLSearchParams (modern approach) +function extractParamsModern(url) { + try { + const urlObj = new URL(url) + return Object.fromEntries(urlObj.searchParams) + } catch (e) { + return {} + } +} +// Test cases: +console.log(`Exercise 16: URL Parameter Extractor`) +console.log(extractParams("https://example.com/page?name=john&age=30")) +// { name: "john", age: "30" } + +console.log(extractParams("https://example.com/search?q=javascript&lang=en&sort")) +// { q: "javascript", lang: "en", sort: "" } + +// Case 1 +console.log(extractParams("https://example.com/search?search=javascript%20tutorial&page=1&sort=&filter=")) +// { search: "javascript%20tutorial", page: "1", sort: "", filter: "" } + +// Case 2 +console.log(extractParams("https://example.com/page?user=admin&logged=true&token=abc123&remember")) +// { user: "admin", logged: "true", token: "abc123", remember: "" } + +// Case 3 +console.log(extractParams("https://example.com/page?email=user@example.com&tags=js&tags=react&premium=&redirect=/dashboard")) +// { email: "user@example.com", tags: "js", premium: "", redirect: "/dashboard" } +console.log("\n==================================\n") + +/* +Exercise 17: Hashtag Extractor +Difficulty: Medium +Objective: Find all hashtags in text +*/ +// Requirements: +// [x] - Find all words starting with # +// [x] - Return array of hashtags (without #) +// [x] - No duplicates +// [x] - Case insensitive comparison +// [x] - "#JavaScript is awesome #javascript #WebDev" → ["javascript", "webdev"] + +/** + * Extracts unique hashtags from text (case-insensitive) + * @param {string} text - Text containing hashtags + * @returns {Array} Array of unique hashtags without # + */ +function extractHashtags(text) { + // Use Set for automatic deduplication + const hashtags = new Set() + + // Split by spaces and check each word + text + .toLowerCase() + .split(/\s+/) + .forEach((word) => { + if (word.startsWith("#") && word.length > 1) { + // Remove # and add to set + hashtags.add(word.slice(1)) + } + }) + + return Array.from(hashtags) +} + +// Alternative: Using RegExp (more robust) +function extractHashtagsRegex(text) { + // Match # followed by one or more word characters + const matches = text.toLowerCase().match(/#(\w+)/g) + + if (!matches) return [] + + // Remove # and deduplicate with Set + return [...new Set(matches.map((tag) => tag.slice(1)))] +} +// Test cases: +console.log("Exercise 17: Hashtag Extractor") +console.log(extractHashtags("#JavaScript is awesome #javascript #WebDev")) +// ["javascript", "webdev"] +console.log(extractHashtags("No hashtags here")) +// [] +console.log( + extractHashtags( + "Just finished an amazing coding session! Built a password validator from scratch using JavaScript. The feeling when your code finally works perfectly is unbeatable. Time to celebrate with some coffee! #coding #javascript #webdevelopment #programming #developerlife #coffeecode", + ), +) +// ["coding", "javascript "webdevelopment", "programming", "developerlife", "coffeecode"] +console.log( + extractHashtags( + "Exploring the beautiful mountains this weekend was exactly what I needed. Fresh air, stunning views, and complete disconnection from #technology. Nature has a way of resetting your mind and soul. Can't wait for the next adventure! #hiking #nature #mountains #adventure #outdoorlife #weekendvibes #naturelover #TECHNOLOGY", + ), +) +// ["hiking", "nature", "mountains", "adventure", "outdoorlife", "weekendvibes", "naturelover", "TECHNOLOGY"] +console.log( + extractHashtags( + "Finally tried that new Italian restaurant downtown and wow, the pasta was incredible! Homemade fettuccine with truffle sauce that melted in my mouth. The tiramisu for dessert was the perfect ending. Highly recommend it to all food lovers! #foodie #italianfood #pasta #restaurant #foodlover #delicious #foodphotography", + ), +) +// [#foodie", #italianfood", #pasta", #restaurant", #foodlover", #delicious", #foodphotography] +console.log("\n==================================\n") + +/* +Exercise 18: Email Extractor +Difficulty: Medium +Objective: Find all email addresses in text +*/ +// Requirements: +// [x] - Extract all valid-looking emails +// [x] - Return array +// [x] - Basic validation: word@word.word pattern + +/** + * Extracts email addresses from text + * @param {string} text - Text containing emails + * @returns {Array} Array of email addresses found + */ +function extractEmails(text) { + // Email RegExp pattern (simplified but functional) + const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g + + const matches = text.match(emailPattern) + + return matches || [] +} + +// More robust version with validation +function extractEmailsRobust(text) { + const emailPattern = /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g + + const matches = text.match(emailPattern) || [] + + // Additional validation: ensure @ and . are present + return matches.filter((email) => { + const parts = email.split("@") + return parts.length === 2 && parts[1].includes(".") && parts[0].length > 0 + }) +} + +// Test cases: +console.log("Exercise 18: Email Extractor") +console.log(extractEmails("Contact us at support@example.com or sales@example.com")) +// ["support@example.com", "sales@example.com"] + +// Case 2: Email with numbers and domains +console.log(extractEmails("Send reports to admin123@mail.company.co.uk and backup to dev@test-server.io")) +// ["admin123@mail.company.co.uk", "dev@test-server.io"] + +// Case 3: Text without emails +console.log(extractEmails("No valid emails here: @example.com, user@, incomplete@")) +// [] + +// Case 4: Only one email +console.log(extractEmails("Hello! Reach me at john.doe@gmail.com, or call me. Thanks!")) +// ["john.doe@gmail.com"] + +// Case 5: Múltiples emails +console.log(extractEmails("Team: alice_smith@company.org, bob-jones@startup.tech, info@support.net and ceo@business.com.")) +// ["alice_smith@company.org", "bob-jones@startup.tech", "info@support.net", "ceo@business.com"] +console.log("\n==================================\n") + +/* +Exercise 19: Find Longest Word +Difficulty: Easy +Objective: Return longest word and its length +*/ +// Requirements: +// [x] - Ignore punctuation +// [x] - Return object: { word: "longest", length: 7 } +// [x] - If tie, return first occurrence + +/** + * Finds the longest word in a sentence + * @param {string} sentence - Sentence to analyze + * @returns {Object} Object with longest word and its length + */ +function findLongestWord(sentence) { + // Remove punctuation and split into words + const words = sentence + .replace(/[^\w\s]/g, "") // Keep only letters, numbers, spaces + .trim() + .split(/\s+/) // Split by whitespace + .filter((word) => word.length > 0) // Remove empty strings + + // Handle empty input + if (words.length === 0) { + return { word: "", length: 0 } + } + + // Find longest using reduce + const longest = words.reduce((longest, current) => { + return current.length > longest.length ? current : longest + }) + + return { word: longest, length: longest.length } +} + +// Alternative: Your approach (also excellent) +function findLongestWordYourWay(sentence) { + const words = sentence + .replace(/[^\w\s]/g, "") + .trim() + .split(/\s+/) + + let longestWord = { word: "", length: 0 } + + for (const word of words) { + if (word.length > longestWord.length) { + longestWord = { word: word, length: word.length } + } + } + + return longestWord +} +// Test cases: +console.log("Exercise 19: Find Longest Word") +console.log(findLongestWord("The quick brown fox jumps")) +// { word: "quick", length: 5 } or { word: "brown", length: 5 } or { word: "jumps", length: 5 } +console.log(findLongestWord("JavaScript is amazing!")) +// { word: "JavaScript", length: 10 } + +// Case 1 +console.log(findLongestWord("The extraordinary development environment provides unbelievable performance")) +// { word: "extraordinary", length: 13 } + +// Case 2 +console.log(findLongestWord("It's a well-documented state-of-the-art framework")) +// { word: "documented", length: 10x } + +// Case 3 +console.log(findLongestWord("Supercalifragilisticexpialidocious")) +// { word: "Supercalifragilisticexpialidocious", length: 34 } + +// Case 4: Empty +console.log(findLongestWord(" ")) +// { word: "", length: 0 } + +// Case 5 (Bonus) +console.log(findLongestWord("Code123 is shorter than programming456 today!!!")) +// { word: "programming456", length: 14 } +console.log("\n==================================\n") + +/* +Exercise 20: Text Between Delimiters +Difficulty: Medium +Objective: Extract text between two markers +*/ +// Requirements: +// [x] - Extract all text between start and end markers +// [x] - Return array +// [x] - "Hello [name], welcome to [place]" with markers "[" and "]" +// → ["name", "place"] + +/** + * Extracts all text between delimiters + * @param {string} text - Text to search + * @param {string} start - Start delimiter + * @param {string} end - End delimiter + * @returns {Array} Array of matches between delimiters + */ +function extractBetween(text, start, end) { + // Escape special RegExp characters in delimiters + const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + + const escapedStart = escapeRegExp(start) + const escapedEnd = escapeRegExp(end) + + // Build pattern: start + (capture anything except end) + end + const pattern = new RegExp(`${escapedStart}([^${escapedEnd}]+?)${escapedEnd}`, "g") + + const matches = [] + let match + + // Use exec() in loop to get all matches with capture groups + while ((match = pattern.exec(text)) !== null) { + matches.push(match[1]) // Capture group 1 = text between delimiters + } + + return matches +} + +// Alternative: Simpler for single-character delimiters +function extractBetweenSimple(text, start, end) { + // Only works reliably for single characters + const regex = new RegExp(`\\${start}(.*?)\\${end}`, "g") + const matches = [] + let match + + while ((match = regex.exec(text)) !== null) { + matches.push(match[1]) + } + + return matches +} + +// Test cases: +console.log("Exercise 20: Text Between Delimiters") +console.log(extractBetween("Hello [name], welcome to [place]", "[", "]")) +// ["name", "place"] +console.log(extractBetween("The {quick} brown {fox}", "{", "}")) +// ["quick", "fox"] + +console.log(extractBetween("I love {JS} language in {VisualStudio Code}", "{", "}")) +console.log("\n==================================\n") + +/** + * + * +// 1. Remove all non-alphanumeric +str.replace(/[^a-z0-9]/gi, '') + +// 2. Remove all non-digits +str.replace(/\D/g, '') + +// 3. Remove all punctuation +str.replace(/[^\w\s]/g, '') + +// 4. Check if contains uppercase +/[A-Z]/.test(str) + +// 5. Check if contains lowercase +/[a-z]/.test(str) + +// 6. Check if contains number +/[0-9]/.test(str) + +// 7. Split by multiple spaces +str.split(/\s+/) + +// 8. Remove consecutive characters +str.replace(/-+/g, '-') + +// 9. Match email pattern +/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i + +// 10. Match URL parameters +/[?&]([^=]+)=([^&]*)/g + */ + +// Master Array +// Instead of: +/*let result = [] +for (const item of array) { + if (condition) result.push(transform(item)) +} + +// Use: +const result = array.filter(condition).map(transform) + +Learn: + +.map() - Transform each element +.filter() - Keep only matching elements +.reduce() - Combine into single value +.find() - Get first match +.some() / .every() - Boolean checks +*/ diff --git a/fullstack/javascript/1-variables-strings/exercises/script-regexp.js b/fullstack/javascript/1-variables-strings/exercises/script-regexp.js new file mode 100644 index 0000000..ffca49f --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/script-regexp.js @@ -0,0 +1,195 @@ +/* +1. Remove all non-digits +str.replace(/\D/g, '') +"abc123def456" → "123456" + +2. Remove all non-alphanumeric (keep letters and numbers) +str.replace(/[^\w]/g, '') +"hello-world!" → "helloworld" + +3. Remove punctuation (keep letters, numbers, spaces) +str.replace(/[^\w\s]/g, '') +"Hello, world!" → "Hello world" + +4. Split by one or more spaces +str.split(/\s+/) +"hello world" → ["hello", "world"] + +5. Match email pattern +/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g + +6. Match hashtags +/#(\w+)/g + +7. Match text between delimiters (non-greedy) +/\[(.*?)\]/g +"[abc] [def]" → matches "abc" and "def" separately + +8. Check if contains uppercase +/[A-Z]/.test(str) + +9. Check if contains number +/\d/.test(str) + +10. Escape RegExp special characters +str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + +Challenge for You +Find potential bugs in these other solutions I gave you: +Exercise 13: Title Case Converter +javascript// Does this handle ALL edge cases? +const prevWord = words[index - 1]; +if (prevWord && prevWord.endsWith(':')) { + return capitalize(word); +} +What if: + +Input: "title:no space after colon" +Input: "title: :double colon" +Input: "title:)emoticon" + +Exercise 16: URL Parameter Extractor +javascript +const queryString = url.slice(queryIndex + 1); +What if: + +Input: "https://example.com?" +Input: "https://example.com?key=value=extra" +Input: "https://example.com?key=" + +Test these and let me know if you find bugs! +*/ + +/* Testing */ +const myName = " Yoandy 88 " +console.log("Eliminando no digitos:" + myName.replace(/\D/g, "")) +console.log("Eliminando digitos:" + myName.replace(/\d/g, "")) +console.log("Eliminando espacios:" + myName.replace(/\s+/g, "")) +console.log("Eliminando letras - punctuation:" + "Hello, world 22587!".replace(/\D/g, "")) + +/* +Exercise 6: Credit Card Masker +**Difficulty:** Easy +**Objective:** Mask credit card number showing only last 4 digits +Requirements: +[x] - Accept string of 16 digits +[x] - Replace first 12 digits with * +[x] - Keep spaces if present +[x] - "1234 5678 9012 3456" becomes "**** **** **** 3456" +*/ + +/** + * Function that receive a credit card number and apply a mask showing only last 4 digits. + * @param {String} cardNumber credit card number with 16 or 19 digits. + * @returns credit card number showing only last 4 digits. + */ +function maskCreditCard(cardNumber) { + // Keep numbers only + //const cleaned = cardNumber.replace(/[^0-9]/g, "") + const cleaned = cardNumber.replace(/\D/g, "") + // check length + if (cleaned.length !== 16) { + console.log("only digits:" + cleaned) + return "Wrong credit card number" + } + // Mask first 12 digits + const masked = "*".repeat(12) + cleaned.slice(-4) + // Re-add spaces if original had them + if (cardNumber.includes(" ")) { + return masked.match(/.{1,4}/g).join(" ") + } + return masked +} + +// Test cases: +console.log(`Exercise 6: Credit Card Masker`) +console.log(maskCreditCard("1234567899876549")) // "************3456" +console.log(maskCreditCard("1234 5678 9012 3456")) // "**** **** **** 3456" +// Valid cases +console.log(maskCreditCard("1234567890123456")) // "************3456" +console.log(maskCreditCard("1234 9632 9012 3456")) // "**** **** **** 3456" +console.log(maskCreditCard("4532-1488-0343-6467")) // "**** **** **** 6467" + +// Invalid cases +console.log(maskCreditCard("yoandydobleshola")) // "Wrong credit card number" +console.log(maskCreditCard("123")) // "Wrong credit card number" +console.log(maskCreditCard("abcd1234efgh5678")) // "Wrong credit card number" +console.log(maskCreditCard("")) // "" (string vacío)*/ +console.log("\n==================================\n") + +/* +Find potential bugs in these other solutions I gave you: +Exercise 13: Title Case Converter +javascript// Does this handle ALL edge cases? +const prevWord = words[index - 1]; +if (prevWord && prevWord.endsWith(':')) { + return capitalize(word); +} +What if: + +Input: "title:no space after colon" +Input: "title: :double colon" +Input: "title:)emoticon" +Exercise 13: Title Case Converter (Advanced) +Difficulty: Hard +Objective: Convert to title case with English grammar rules +*/ + +// Requirements: +// [x] - Capitalize first and last word always +// [x] - Capitalize all words except: a, an, the, and, but, or, for, nor, on, at, to, by, in +// [x] - Articles after colon (:) should be capitalized +// [x] - "the quick brown fox" → "The Quick Brown Fox" +// [x] - "a tale of two cities" → "A Tale of Two Cities" + +/** + * Converts text to title case following English grammar rules + * @param {string} text - Text to convert + * @returns {string} Title-cased text + */ +function toTitleCase(text) { + // Words that should remain lowercase (unless first/last/after colon) + const exceptions = new Set(["a", "an", "the", "and", "but", "or", "for", "nor", "on", "at", "to", "by", "in", "of"]) + + const words = text.trim().toLowerCase().split(/\s+/) + + return words + .map((word, index) => { + // Always capitalize first and last word + if (index === 0 || index === words.length - 1) { + return capitalize(word) + } + + // Check if previous word ended with colon + const prevWord = words[index - 1] + if (prevWord && prevWord.endsWith(":")) { + return capitalize(word) + } + + // Capitalize unless it's an exception word + return exceptions.has(word) ? word : capitalize(word) + }) + .join(" ") +} + +/** + * Capitalizes first letter of a word + * @param {string} word - Word to capitalize + * @returns {string} Capitalized word + */ +function capitalize(word) { + return word.charAt(0).toUpperCase() + word.slice(1) +} +// Test cases: +console.log(`Exercise 13: Title Case Converter (Advanced)`) +//console.log(toTitleCase("the lord of the rings")) +// "The Lord of the Rings" +//console.log(toTitleCase("thE an THE")) +// "The an The" +//console.log(toTitleCase("title:no space after colon")) +//console.log(toTitleCase("title: :double colon")) +console.log(toTitleCase("title:'-(emotic:(on")) +// "Javascript" +//console.log(toTitleCase(" Array sentence: just finished an amazing dayx. ")) +console.log("\n==================================\n") diff --git a/fullstack/javascript/1-variables-strings/exercises/script.js b/fullstack/javascript/1-variables-strings/exercises/script.js new file mode 100644 index 0000000..c6d5983 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/script.js @@ -0,0 +1,1487 @@ +/* +String Exercises - JavaScript Mastery +Author: Yoandy Doble Herrera +Date: 11/01/2026 +freeCodeCamp String Quiz Score: 20/20 (100%) +*/ + +// ======================================== +// PART 1: String Analysis & Validation +// ======================================== + +/* +Session 1 + +Exercise 1: Email Validator +Difficulty: Medium +Objective: Create a function that validates email format + +Requirements: +[x] - Must contain exactly one @ +[x] - Must have characters before and after @ +[x] - Must have a dot (.) after @ +[x] - Must not start or end with whitespace +[x] - Return true/false +*/ + +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmail(email) { + let emailTemp = `` + // Step 1: Check start or end with whitespace + if (email[0] !== " " && email[email.length - 1] !== " ") { + // Step 2: Convert in array by @ + emailTemp = email.split("@") + //Must contain exactly one @ + //Must have characters before and after @ + // Must have a dot (.) after @ + if (emailTemp.length === 2 && emailTemp[1].includes(".") && emailTemp[0].length > 1 && emailTemp[1].length > 1) { + return true + } else { + return false + } + } else { + return false + } +} + +// Test cases: +console.log(`Exercise 1: Email Validator`) +console.log(isValidEmail("user@.")) // false +console.log(isValidEmail(" invalid.email")) // false +console.log(isValidEmail(" spaced@email.com ")) // false +console.log(isValidEmail("no@domain ")) // false +console.log(isValidEmail("code.claude@domain.fr @ ca")) //false +console.log(isValidEmail("yoandy.doble@gmail.com")) //true +console.log("\n==================================\n") + +/* +Exercise 1 - Method 2: Email Validator +Difficulty: Medium +Objective: Create a function that validates email format +*/ + +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmailMethodTwo(email) { + let emailTemp = `` + const regExp = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/g + // Step 1: Check start or end with whitespace + if (email.localeCompare(email.trim()) === 0) { + // Step 2: Apply regExp with ternary + return regExp.test(email) ? true : false + } else { + return false + } +} + +// Test cases: +console.log(`Exercise 1 - Method 2: Email Validator`) +console.log(isValidEmailMethodTwo("user@.")) // false +console.log(isValidEmailMethodTwo(" invalid.email")) // false +console.log(isValidEmailMethodTwo(" spaced@email.com ")) // false +console.log(isValidEmailMethodTwo("no@domain ")) // false +console.log(isValidEmailMethodTwo("code.claude@domain.fr @ ca")) //false +console.log(isValidEmailMethodTwo("yoandy.doble@gmail.com")) //true +console.log("\n==================================\n") + +//Exercise 1 - Email Validator SOLVED CLAUDE +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmailThree(email) { + // check for whitespaces + if (email !== email.trim()) { + return false + } + // Split by @ - must be exactly 2 parts + const parts = email.split("@") + if (parts !== 2) { + return false + } + const [local, domain] = parts + + // Both parts must exist and domain must have a dot + return local.length > 0 && domain.length > 0 && domain.includes(".") +} + +//Exercise 1 - Email Validator SOLVED CLAUDE +/** + * Function that validates email format. + * @param {String} email An electronic email to be validate. + * @retun true if it's a valid email or false if it's not. + */ +function isValidEmailFour(email) { + if (email !== email.trim()) return false + return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/.test(email) +} + +// Test cases: +console.log(`Exercise 1: Email Validator`) +console.log(isValidEmailThree("user@.")) // false +console.log(isValidEmailThree(" invalid.email")) // false +console.log(isValidEmailThree(" spaced@email.com ")) // false +console.log(isValidEmailThree("no@domain ")) // false +console.log(isValidEmailThree("code.claude@domain.fr @ ca")) //false +console.log(isValidEmailThree("yoandy.doble@gmail.com")) //true +console.log("\n==================================\n") + +/* +Exercise 5: Username Sanitizer +**Difficulty:** Easy-Medium +**Objective:** Clean and validate username input +*/ + +// Requirements: +// [x] - Remove whitespace from start/end +// [x] - Replace spaces with underscores +// [x] - Convert to lowercase +// [x] - Remove special characters (keep only letters, numbers, underscore) +// [x] - Ensure length is 3-20 characters +// [x] - Return sanitized username or null if invalid + +/* +Roadmap +- check length 3-20 characters: continue or return null +- use toLowerCase +- use trim method to remove whitespace from start/end +- New function: Remove special characters, keep underscores and convert to whitespace. +- Keep single spaces. +- use replaceAll whitespaces to undercores +- Return sanitized username or null if invalid +*/ + +/** + * Function that clean and validate username input. + * @param {String} input any string username. + * @returns sanitized username or null if invalid. + */ +function sanitizeUsername(input) { + if (input.length >= 3 && input.length <= 20) { + let userName = removeSpecialCharacters(input.trim().toLowerCase()) + return applySpaces(userName).replaceAll(" ", "_") + } else { + return null + } +} + +/** + * Function that remove special characters from a string and convert to whitespace. + * @param {String} userName userName input to remove special characters if presented. + * @returns {String} username without special characters. + */ +function removeSpecialCharacters(userName) { + let userNameArray = [] + // prettier-ignore + const specialSymbols = ["!", "@", "#", "$", "%", "&", "*", "(", ")", "-", "=", "+", "[", "]", "{", "}", "|", "\\", ";", ":", "'", "\"", ",", "<", ".", ">", "/", "?", "~", "^", "+", "-", "×", "÷", "±", "≠", "≤", "≥", "≈", "≈", "∫", "∑", "∏", "√", "∞", "≈", "≡", "≅", "≠", "≤", "≥", "∈", "∉", "⊂", "⊃", "⊆", "⊇", "∩", "∪", "∧", "∨", "¬", "∀", "∃", "∅", "∇", "∂", "′", "″", "‴", "€", "£", "¥", "¢", "₣", "₹", "₽", "₩", "₺", "₫", "₱", "฿", "ℓ", "µ", "°", "′", "″", "§", "¶", "©", "®", "™", "€", "¥", "¢", "£", "₣", "₹", "₽", "₩", "₺", "₫", "₱", "฿", "ℓ", "µ", "°", "′", "″", "§", "¶", "©", "®", "™"] + for (const character of userName) { + if (!specialSymbols.includes(character)) { + userNameArray.push(character) + } else { + userNameArray.push(" ") + } + } + return userNameArray.join("") +} + +/** + * Function that sanitize spaces. + * @param {String} userName userName input to remove double spaces. + * @returns {String} username with spaces. + */ +function applySpaces(userName) { + const userNameArray = userName.split(" ") + let users = [] + for (const word of userNameArray) { + if (word.length > 0) { + users.push(word) + } + } + return users.join(" ") +} + +// Test cases: +console.log(`Exercise 5: Username Sanitizer`) +console.log(sanitizeUsername(" John Doe!@# ")) // "john_doe" +console.log(sanitizeUsername("ab")) // null (too short) +console.log(sanitizeUsername("User Name 123")) // "user_name_123" +console.log(sanitizeUsername(" jane_doe@2025")) // "user_name_123" +console.log(sanitizeUsername("M ike_ Lucy@home ")) // "mike_lucy_home" +console.log(sanitizeUsername(" emily&tom @home!")) // "emily_tom_home" +console.log(sanitizeUsername(" David @team#2025")) // "david_team_2025" +console.log(sanitizeUsername(" Sarah-2026!")) // sarah_2026 +console.log(sanitizeUsername("longterm-androidstudio-Iphone.Visual-Studio-Code_All In One")) // null (too long) +console.log("\n==================================\n") + +// Exercise 5: Username Sanitizer || Claude AI SOLVED +/** + * Function that sanitize spaces. + * @param {String} userName userName input to remove double spaces. + * @returns {String} username with spaces. + */ +function sanitizeUsernameAI(input) { + // Remove whitespace, lowercase + const cleaned = input.trim().toLowerCase() + + // Remove special chars (keep letters, numbers, spaces) + const alphanumeric = cleaned.replace(/[^a-z0-9\s]/g, "") + + // Replace spaces with underscores, remove consecutive underscores + const username = alphanumeric.replace(/\s+/g, "_") + + // Check length + return username.length >= 3 && username.length <= 20 ? username : null +} + +// Test cases: +console.log(`Exercise 5: Username Sanitizer AI`) +console.log(sanitizeUsernameAI(" John Doe!@# ")) // "john_doe" +console.log(sanitizeUsernameAI("ab")) // null (too short) +console.log(sanitizeUsernameAI("User Name 123")) // "user_name_123" +console.log(sanitizeUsernameAI(" jane_doe@2025")) // "user_name_123" +console.log(sanitizeUsernameAI("M ike_ Lucy@home ")) // "mike_lucy_home" +console.log(sanitizeUsernameAI(" emily&tom @home!")) // "emily_tom_home" +console.log(sanitizeUsernameAI(" David @team#2025")) // "david_team_2025" +console.log(sanitizeUsernameAI(" Sarah-2026!")) // sarah_2026 +console.log(sanitizeUsernameAI("longterm-androidstudio-Iphone.Visual-Studio-Code_All In One")) // null (too long) +console.log("\n==================================\n") +/* +Exercise 6: Credit Card Masker +**Difficulty:** Easy +**Objective:** Mask credit card number showing only last 4 digits +*/ + +// Requirements: +// [x] - Accept string of 16 digits +// [x] - Replace first 12 digits with * +// [x] - Keep spaces if present +// [x] - "1234 5678 9012 3456" becomes "**** **** **** 3456" + +/** + * Function that receive a credit card number and apply a mask showing only last 4 digits. + * @param {String} cardNumber credit card number with 16 or 19 digits. + * @returns credit card number showing only last 4 digits. + */ +function maskCreditCard(cardNumber) { + if (cardNumber.length === 16) { + // Accept string of 16 digits + return replaceDigits(cardNumber) + // Accept string of 19 digits with spaces + } else if (cardNumber.split(" ").length === 4) { + // Keep spaces if present + return replaceDigitsSpaces(cardNumber) + } else { + return `Invalid credit card number: ${cardNumber} \nPlease insert a valid credit card.` + } +} + +/** + * Function that receives a credit card number and replace first 12 digits with *. + * @param {String} creditCard credit card number with 16 digits. + * @returns credit card number showing only last 4 digits. + */ +function replaceDigits(creditCard) { + let creditCardArray = [] + for (let index = 0; index < creditCard.length; index++) { + if (index <= 11) { + creditCardArray.push("*") + } else { + creditCardArray.push(creditCard[index]) + } + } + return creditCardArray.join("") +} + +/** + * Function that receives a credit card number and replace first 12 digits with *. Keep spaces. + * @param {String} creditCard credit card number with 19 digits. + * @returns credit card number showing only last 4 digits. + */ +function replaceDigitsSpaces(creditCard) { + let cardNumberMask = [] + for (let index = 0; index < creditCard.length; index++) { + if (index <= 14) { + if (creditCard[index] !== " ") { + cardNumberMask.push("*") + } else { + cardNumberMask.push(" ") + } + } else { + // visible card numbers + cardNumberMask.push(creditCard[index]) + } + } + return cardNumberMask.join("") +} +// Test cases: +console.log(`Exercise 6: Credit Card Masker`) +console.log(maskCreditCard("1234567899876549")) // "************3456" +console.log(maskCreditCard("1234 5678 9012 3456")) // "**** **** **** 3456" +// Valid cases +console.log(maskCreditCard("1234567890123456")) // "************3456" +console.log(maskCreditCard("1234 9632 9012 3456")) // "**** **** **** 3456" + +// Invalid cases +console.log(maskCreditCard("4532-1488-0343-6467")) // "Wrong credit card number" +console.log(maskCreditCard("123")) // "Wrong credit card number" +console.log(maskCreditCard("abcd1234efgh5678")) // "Wrong credit card number" +console.log(maskCreditCard("")) // "" (string vacío)*/ +console.log("\n==================================\n") + +/* +Exercise 7: Initials Extractor +**Difficulty:** Easy +**Objective:** Extract initials from full name +*/ + +// Requirements: +// [x] - Handle multiple middle names +// [x] - Return uppercase initials with dots +// [x] - "john paul jones" → "J.P.J." +// [x] - Handle extra spaces + +/** + * Function that extract initials from full name. + * @param {String} fullName name and last name or name, middle name and last name. + * @returns {String} Return uppercase initials from full name with dots. + */ +function getInitials(fullName) { + // Your code here + const fullNameSplitted = fullName.trim().toUpperCase().split(" ") + // Check extra spaces + if (fullNameSplitted.length === 2 || fullNameSplitted.length === 3) { + //Full name normal case: name + last name || name + middle name + last name + return initials(fullNameSplitted) + } else { + // Full name extra spaces + return removeExtraSpaces(fullNameSplitted) + } +} + +/** + * Function that extract initials from an array. + * @param {Array} fullName name and last name or name, middle name and last name. + * @returns {String} Return uppercase initials from array with dots. + */ +function initials(fullName) { + let initialsName = [] + for (const names of fullName) { + initialsName.push(names[0]) + } + return initialsName.join(".") + "." +} + +/** + * Function that extract initials from an array. + * @param {Array} fullName name and last name with middle spaces or name, middle name and last name with middle spaces. + * @returns {String} Return uppercase initials from array with dots. + */ +function removeExtraSpaces(fullName) { + let initialsName = [] + for (const names of fullName) { + if (names.length > 0) initialsName.push(names[0]) + } + return initialsName.join(".") + "." +} + +// Test cases: +console.log(`Exercise 7: Initials Extractor`) +console.log(getInitials("John Doe")) // "J.D." +console.log(getInitials("Mary Jane Watson")) // "M.J.W." +console.log(getInitials("Yoandy Doble Herrera")) // "Y.D.H." +console.log(getInitials(" Arthur Conan ")) // "A.C." +console.log(getInitials(" josé maría lópez ")) // "J.M.L." +console.log(getInitials(" Leonel Messi Cuchitiny ")) // "L.M.C." +console.log("\n==================================\n") + +// ======================================== +// PART 2: String Transformation +// ======================================== + +//Session 2 +//PART 2: + +/* +Exercise 3: Palindrome Detective +Difficulty: Medium +Objective: Check if string is palindrome (ignore spaces, punctuation, case) +*/ +// Requirements: +// [x] - Ignore spaces, punctuation, and case +// [x] - "A man, a plan, a canal: Panama" should return true +// [x] - Use trim, toLowerCase, replace + +/** + * Function that check if string is palindrome (ignore spaces, punctuation, case). + * @param {String} str input words with spaces and punctuation. + * @returns {Boolean} true if palindrome or false if not. + */ +function isPalindrome(str) { + // Use trim, toLowerCase + const strLowerCase = str.trim().toLowerCase() + const strArray = strLowerCase.split(" ") + if (strArray.length === 1) { + // Single word ignore spaces and punctuations + const strNoPuntuations = removePunctuations(strArray[0]) + const reversed = reverseStr(strNoPuntuations) + return strNoPuntuations === reversed ? true : false + } else { + // Multiple words ignore spaces and punctuations. + // Use replace + const strReplaced = strLowerCase.replace(" ", "-") + const strNoPuntuations = removePunctuations(strReplaced) + const strNoSpaces = removeSpaces(strNoPuntuations) + const reversed = reverseStr(strNoSpaces) + return strNoSpaces === reversed ? true : false + } +} + +/** + * Function that remove punctuations from string. + * @param {String} str words without start/end space and lowerCase. + * @returns {String} string words without punctuations. + */ +function removePunctuations(str) { + // prettier-ignore + const punctuationSymbols = ["!", "@", "#", "$", "%", "&", "*", "(", ")", "-", "_", "=", "+", "[", "]", "{", "}", "|", "\\", ";", ":", "'", "\"", ",", "<", ".", ">", "/", "?", "~", "^"] + let strEmptyPunctuations = [] + for (let index = 0; index < str.length; index++) { + if (!punctuationSymbols.includes(str[index])) { + strEmptyPunctuations.push(str[index]) + } else { + strEmptyPunctuations.push(" ") + } + } + return strEmptyPunctuations.join("") +} + +/** + * Function that remove spaces from string. + * @param {String} str words without start/end space and lowerCase. + * @returns {String} string words without spaces. + */ +function removeSpaces(str) { + let strEmptySpaces = [] + for (let index = 0; index < str.length; index++) { + if (str[index] !== " ") { + strEmptySpaces.push(str[index]) + } + } + return strEmptySpaces.join("") +} + +/** + * Function that reverse words. + * @param {String} str words without start/end space and lowerCase. + * @returns {String} string words reversed. + */ +function reverseStr(str) { + let count = str.length - 1 + let strReversed = [] + while (count >= 0) { + strReversed.push(str[count]) + count-- + } + return strReversed.join("") +} + +// Test cases: +console.log(`Exercise 3: Palindrome Detective`) +console.log(isPalindrome("racecar}")) // true +console.log(isPalindrome("A man, a plan, a canal: Panama")) // true +console.log(isPalindrome("hello")) // false +console.log(isPalindrome("Was it a car or a cat I saw?")) // true +console.log(isPalindrome("-Javascript-")) // false +console.log(isPalindrome("Wat is your name?")) // false +console.log("\n==================================\n") + +/* +Exercise 4: Word Frequency Counter +Difficulty: Medium +Objective: Count how many times each word appears +*/ +// Requirements: +// [x] - Case insensitive +// [x] - Ignore punctuation +// [x] - Return object with word counts sorted by frequency +// [x] - Use toLowerCase, trim, replace, split + +/** + * Function that count how many times each word appears. + * @param {String} text any sentence. + * @returns {Object} object with word counts sorted by frequency. + */ +function wordFrequency(text) { + // Step 1: Use toLowerCase, trim, replace, split + const sentenceArray = text + .trim() + .toLowerCase() + .replace(/[#%&=<>~^-_.,!?;:*+\\[\]{}()@'"|/$]/g, "") + .split(" ") + // Step 2: Count frequencies + let wordCounts = {} + for (const word of sentenceArray) { + if (Object.hasOwn(wordCounts, word)) { + // Repeated count++ + let countUp = wordCounts[word] + countUp++ + wordCounts[word] = countUp + } else { + // First time add + wordCounts[word] = 1 + } + } + // Step 3: array descendant sort + const ordered = Object.entries(wordCounts).sort(([, a], [, b]) => b - a) + // convert array to object again + return Object.fromEntries(ordered) +} + +// Exercise 4 Method 2 +/** + * Function that count how many times each word appears. + * @param {String} text any sentence. + * @returns {Object} object with word counts sorted by frequency. + */ +function wordFrequencyTwo(text) { + // Step 1: Use toLowerCase, trim, replace, split + const sentenceArray = text + .trim() + .toLowerCase() + .replace(/[#%&=<>~^-_.,!?;:*+\\[\]{}()@'"|/$]/g, "") + .split(" ") + // Step 2: Count frequencies + const wordCounts = {} + for (const word of sentenceArray) { + wordCounts[word] = (wordCounts[word] || 0) + 1 + } + // Step 3: array descendant sort + const ordered = Object.entries(wordCounts).sort(([, a], [, b]) => b - a) + // convert array to object again + return Object.fromEntries(ordered) +} + +// Test case: +console.log(`Exercise 4: Word Frequency Counter`) +console.log(wordFrequency("The quick brown fox jumps over the lazy dog. The dog was really lazy.")) +console.log(wordFrequencyTwo("The quick brown fox jumps over the lazy dog. The dog was really lazy.")) + +// { the: 2, lazy: 2, dog: 2, quick: 1, brown: 1, ... } +console.log("\n==================================\n") + +/* +Exercise 9: Slug Generator +Difficulty: Medium +Objective: Convert blog post title to URL-friendly slug +*/ +// Requirements: +// [x] - Convert to lowercase +// [x] - Replace spaces with hyphens +// [x] - Remove special characters except hyphens +// [x] - Remove consecutive hyphens +// [x] - Trim hyphens from start/end +// [x] - "Hello World! How Are You?" → "hello-world-how-are-you" + +/** + * Function that convert blog post title to URL-friendly slug. + * @param {String} title Blog post title. + * @returns {String} URL-friendly slug. + */ +function generateSlug(title) { + const titleTrim = title.trimStart().trimEnd().toLowerCase().replaceAll(" ", "-") + const titleNoChars = removeSpecialChars(titleTrim) + const urlSlug = removeDuplicateHyphens(titleNoChars) + return urlSlug +} + +/** + * Function that remove special characters except hyphens. + * @param {String} title title without start/end space and lowerCase. + * @returns {String} title without punctuations except hyphens. + */ +function removeSpecialChars(title) { + // prettier-ignore + const punctuationSymbols = ["!", "@", "#", "$", "%", "&", "*", "(", ")", "_", "=", "+", "[", "]", "{", "}", "|", "\\", ";", ":", "'", "\"", ",", "<", ".", ">", "/", "?", "~", "^"] + let strEmptyPunctuations = [] + for (let index = 0; index < title.length; index++) { + if (!punctuationSymbols.includes(title[index])) { + strEmptyPunctuations.push(title[index]) + } + } + return strEmptyPunctuations.join("") +} + +/** + * Function that remove consecutive hyphens. + * @param {String} title title without special characters except hyphens, start/end space and in lowerCase. + * @returns {String} title without consecutive hyphens. + */ +function removeDuplicateHyphens(title) { + const titleArray = title.split("-") + let titleSlugArray = [] + let titleSlug = `` + for (const element of titleArray) { + if (element.length > 0) { + titleSlugArray.push(element) + } + } + titleSlug = titleSlugArray.join("-") + return titleSlug +} + +// Test cases: +console.log(`Exercise 9: Slug Generator`) +console.log(generateSlug("My First Blog Post!")) +// "my-first-blog-post" +console.log(generateSlug(" JavaScript Tips & Tricks ")) +// "javascript-tips-tricks" +console.log(generateSlug(" codebydoble @ qvapay .me ")) +// "codebydoble-qvapay-me" +console.log(generateSlug(" url-page.com ")) +// "codebydoble-qvapay-me" +console.log("\n==================================\n") + +/* +Exercise 14: Reverse Words (Keep Order) +Difficulty: Easy +Objective: Reverse each word but keep word order +*/ +// Requirements: +// [x] - "Hello World" → "olleH dlroW" +// [x] - Preserve spacing +// [x] - Use split, reverse (array method), join + +/** + * Function that reverse each word but keep word order and preserve spacing. + * @param {String} sentence a word or paragraph. + * @returns a reversed sentence. + */ +function reverseWords(sentence) { + // Use split + //nonsense reverse change original sentence + const sentenceArray = sentence.split(" ") + let sentenceReversed = `` + let sentenceArrayJoin = [] + if (sentenceArray.length === 1) { + // one word + if (sentenceArray[0].length > 0) { + // check at least a silaba + return reverseStr(sentenceArray[0]) + } else { + console.log("Error: no word found.") + } + } else { + for (const word of sentenceArray) { + sentenceArrayJoin.push(reverseStr(word)) + } + sentenceReversed = sentenceArrayJoin.join(" ") + return sentenceReversed + } +} + +// Test cases: +console.log(`Exercise 14: Reverse Words (Keep Order)`) +console.log(reverseWords("")) +console.log(reverseWords(" Barcelona")) +console.log(reverseWords("Hello World")) // "olleH dlroW" +console.log(reverseWords("JavaScript is awesome")) // "tpircSavaJ si emosewa" +console.log(reverseWords("JavaScript is awesome")) // "tpircSavaJ si emosewa" +console.log("\n==================================\n") + +//Session 3 +//PART 3: String Search & Extract + +/* +Exercise 2: Password Strength Checker +Difficulty: Hard +Objective: Analyze password strength and return detailed feedback +Requirements: +[x] - Check for: length (8+ chars), uppercase, lowercase, numbers, special chars +[x] - Return object: { strength: "weak/medium/strong", missing: [...], score: 0-5 } +[x] - Use multiple string methods +*/ + +/** + * Function that analyze password strength. Check for: length (8+ chars), uppercase, lowercase, numbers and special chars. + * @param {String} password string password to check. + * @returns {Object} return an object with detailed feedback about password. + */ +function checkPasswordStrength(password) { + let strength = `` + let missing = [] + let score = 0 + // prettier-ignore + const uppercaseLetters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] + // prettier-ignore + const lowercaseLetters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] + const numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + // prettier-ignore + const specialChars = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "=", "+", "[", "]", "{", "}", "|", "\\", ";", ":", "'", "\"", ",", "<", ".", ">", "/", "?", "~", "`"] + if (password.length >= 8) { + score++ + } else { + missing.push("length") + } + if (checkPasswordCondition(password, uppercaseLetters) === true) { + score++ + } else { + missing.push("uppercase") + } + if (checkPasswordCondition(password, lowercaseLetters) === true) { + score++ + } else { + missing.push("lowercase") + } + if (checkPasswordCondition(password, numbers) === true) { + score++ + } else { + missing.push("numbers") + } + if (checkPasswordCondition(password, specialChars) === true) { + score++ + } else { + missing.push("special") + } + switch (score) { + case 0: + strength = "weak" + break + case 1: + strength = "weak" + break + case 2: + strength = "weak" + break + case 3: + strength = "medium" + break + case 4: + strength = "medium" + break + default: + strength = "strong" + break + } + return { strength: strength, missing: missing, score: score } +} + +/** + * Function that check for: uppercase, lowercase, numbers and special chars. + * @param {String} password string password to check. + * @param {Array} checkArray uppercase, lowercase, numbers or special chars. + * @returns {Boolean} return true if password includes checkArray. + */ +function checkPasswordCondition(password, checkArray) { + let result = false + for (let index = 0; index < password.length; index++) { + if (checkArray.includes(password[index])) { + result = true + } + } + return result +} + +// Test cases: +console.log(`Exercise 2: Password Strength Checker`) +console.log(checkPasswordStrength("pass")) +// { strength: "weak", missing: ["length", "uppercase", "numbers", "special"], score: 1 } +console.log(checkPasswordStrength("fund1*")) +// { strength: "medium", missing: ["length", "uppercase"], score: 3 } +console.log(checkPasswordStrength("MyP@ssw0rd")) +// { strength: "strong", missing: [], score: 5 } +console.log("\n==================================\n") + +/* +Exercise 8: Sentence Case Converter +Difficulty: Medium +Objective: Convert any text to proper sentence case +*/ +// Requirements: +// [x] - First letter uppercase, rest lowercase +// [x] - After every period, question mark, exclamation: capitalize +// [x] - Preserve single spaces between words +// [x] - "hELLo. hOW ARE you?" → "Hello. How are you?" +// [x] - " hELLo! → "Hello!" trim, lowerCase and after upperCase in first letter +// [x] - " hELLo? → "Hello?" trim, lowerCase and after upperCase in first letter +// [x] - " hELLo. → "Hello." trim, lowerCase and after upperCase in first letter + +/** + * Function that convert any text to proper sentence case. + * @param {String} text any text. + * @returns {String} A proper sentence case. + */ +function toSentenceCase(text) { + const sentence = text.trim().toLowerCase() + const firstLetterUpperCase = sentence[0].toUpperCase() + sentence.slice(1) + const sentenceArray = firstLetterUpperCase.split(" ") + const threePunctuations = [".", "?", "!"] + let toSentenceCase = [] + if (sentenceArray.length === 1) { + // Single word + return firstLetterUpperCase + } else { + // A sentence + for (let index = 0; index < firstLetterUpperCase.length; index++) { + if (threePunctuations.includes(firstLetterUpperCase[index])) { + // Period, question mark or exclamation + // Check final element + if (index < firstLetterUpperCase.length - 1) { + toSentenceCase.push(firstLetterUpperCase[index]) + toSentenceCase.push(" ") + const letterToCapitalize = firstLetterUpperCase[index + 2] + const letterCapitalized = letterToCapitalize.toUpperCase() + toSentenceCase.push(letterCapitalized) + index = index + 2 + } else { + // agregar la puntuacion final + toSentenceCase.push(firstLetterUpperCase[index]) + } + } else { + toSentenceCase.push(firstLetterUpperCase[index]) + } + } + return toSentenceCase.join("") + } +} + +// Test cases: +console.log(`Exercise 8: Sentence Case Converter`) +console.log(toSentenceCase(" hELLo! ")) +console.log(toSentenceCase(" hELLo? ")) +console.log(toSentenceCase(" hELLo. ")) +console.log(toSentenceCase("hELLo WoRLD. hOW aRe YOU? I'm fine! Bye.")) +console.log(toSentenceCase("tHIS is A test! iS it WORking?")) +console.log( + toSentenceCase( + "strings are IMMutable primitives - any operation that appears to modify a string actually creates a new one, leaving the original intact. tHIS ensures data safety? but requires awareness for performance optimization.", + ), +) +console.log("\n==================================\n") + +/* +Exercise 10: Camel Case Converter +Difficulty: Medium +Objective: Convert string to camelCase, PascalCase, snake_case, kebab-case +*/ +// Requirements: +// [x] - One function with 'type' parameter: 'camel', 'pascal', 'snake', 'kebab' +// [x] - Handle multiple input formats +// [x] - "hello world" → "helloWorld" (camel) +// [x] - "hello-world" → "HelloWorld" (pascal) + +/** + * Function that convert string to camelCase, PascalCase, snake_case and kebab-case. + * @param {String} str any sentence. + * @param {String} type parameter: 'camel', 'pascal', 'snake', 'kebab' + * @returns {String} return string converted to type. + */ +function convertCase(str, type) { + const sentence = skipDoubleSpace(str.replace(/[-_.,!?;:*+\\[\]${}()@'"|/$]/g, " ")).split(" ") + switch (type) { + case "camel": + return camelCase(sentence) + case "pascal": + return pascalCase(sentence) + case "snake": + return snakeCase(sentence) + default: + return kebabCase(sentence) + } +} + +/** + * Function that remove double spaces from string. + * @param {String} sentence any single word. + * @returns {String} string without double spaces. + */ +function skipDoubleSpace(sentence) { + const str = [] + for (const word of sentence.split(" ")) { + if (word.length > 0) { + str.push(word) + } + } + return str.join(" ") +} + +/** + * Function that convert string to camelCase. + * @param {Array} str any sentence array. + * @returns {String} return string converted to camelCase. + */ +const camelCase = (str) => { + //camelCase: "helloWorldTest" + if (str.length === 1) { + return str.join("") + } else { + const strArray = [str[0]] + for (let index = 1; index < str.length; index++) { + strArray.push(`${str[index][0].toUpperCase() + str[index].slice(1)}`) + } + return strArray.join("") + } +} + +/** + * Function that convert string to PascalCase. + * @param {Array} str any sentence array. + * @returns {String} return string converted to PascalCase. + */ +const pascalCase = (str) => { + //PascalCase: "HelloWorld" + if (str.length === 1) { + return `${str[0][0].toUpperCase() + str[0].slice(1)}` + } else { + const strArray = [`${str[0][0].toUpperCase() + str[0].slice(1)}`] + for (let index = 1; index < str.length; index++) { + strArray.push(`${str[index][0].toUpperCase() + str[index].slice(1)}`) + } + return strArray.join("") + } +} + +/** + * Function that convert string to snake_case. + * @param {Array} str any sentence array. + * @returns {String} return string converted to snake_case. + */ +const snakeCase = (str) => { + //snake_case: "hello_world" + if (str.length === 1) { + return str.join("") + } else { + return str.join("_") + } +} + +/** + * Function that convert string to kebab-case. + * @param {Array} str any sentence array. + * @returns {String} return string converted to kebab-case. + */ +const kebabCase = (str) => { + //kebab-case: "hello-world" + if (str.length === 1) { + return str.join("") + } else { + return str.join("-") + } +} + +// Test cases: +console.log(`Exercise 10: Camel Case Converter`) +console.log(convertCase("hello world", "camel")) // "helloWorld" +console.log(convertCase("hello world", "pascal")) // "HelloWorld" +console.log(convertCase("hello world", "snake")) // "hello_world" +console.log(convertCase("hello world", "kebab")) // "hello-world" +console.log(convertCase("hello- world-test", "camel")) // "helloWorldTest" +console.log("\n==================================\n") +/* +Exercise 11: Text Abbreviator +Difficulty: Hard +Objective: Shorten text intelligently to specific length +*/ +// Requirements: +// [x] - If text > maxLength, truncate at word boundary (don't cut words) +// [x] - Add "..." at end +// [x] - Preserve at least one word even if it exceeds maxLength +// [x] - Remove trailing punctuation before "..." + +/** + * Function that shorten text intelligently to specific length. + * @param {String} text any sentence. + * @param {Number} maxLength specific length to shorten text. + */ +function abbreviate(text, maxLength) { + const trailingPunctuation = [".", ",", "!", "?", ";", ":", "...", "…", "-", "_", "–", ")", "]", "}", "'", '"', "*", "/"] + if (text.length > maxLength) { + // Truncate + if (text[maxLength - 1] === " ") { + // No truncate word + // check trailing punctuation Example: .) + if (trailingPunctuation.includes(text[maxLength - 2])) { + let textArrayWithTrailingPunct = text.slice(0, maxLength - 1).split(" ") + const lastWord = textArrayWithTrailingPunct.pop() + return `${textArrayWithTrailingPunct.join(" ")} ${removeTrailingPunctuation(lastWord)}...` + } else { + return `${text.slice(0, maxLength)}...` + } + } else { + console.log(text.slice(0, maxLength)) + + // Preserve at least one word even if it exceeds maxLength + const textBeforeIndex = text.slice(0, maxLength).split(" ") + // Delete last element to check trailing puntuation + const lastElementBeforeIndex = textBeforeIndex.pop() + const textAfterIndex = text.slice(maxLength).split(" ") + // Delete first element to check trailing puntuation + const lastElementAfterIndex = textAfterIndex.shift() + return `${textBeforeIndex.join(" ")} ${removeTrailingPunctuation(lastElementBeforeIndex)}${removeTrailingPunctuation(lastElementAfterIndex)}...` + } + } else { + return text + } +} + +/** + * Function that remove trailing punctuation from string. + * @param {String} word any single word. + * @returns {String} an string without trailing punctuation. + */ +function removeTrailingPunctuation(word) { + return word.replace(/[a-zA-Z0-9]/g, "") + + const trailingPunctuation = [".", ",", "!", "?", ";", ":", "...", "…", "-", "_", ")", "]", "}", "'", '"', "*", "/"] + let wordArray = [] + for (const char of word) { + if (!trailingPunctuation.includes(char)) { + wordArray.push(char) + } + } + return wordArray.join("") +} + +// Test cases: +console.log(`Exercise 11: Text Abbreviator`) +//console.log(abbreviate("The quick brown fox...) jumps over the lazy dog", 21)) +// "The quick brown fox..." +//console.log(abbreviate("Hello", 10)) // "Hello" +console.log(abbreviate("This is a very long senten.e that needs truncation", 27)) +// "This is a very long sentence..." +/*console.log( + abbreviate( + "Trailing punctuation refers to punctuation marks (like commas, periods, semicolons, colons, etc.) that appear at the end of a word, phrase, or sentence.", + 93, + ), +)*/ +console.log("\n==================================\n") +/* +Exercise 12: Phone Number Formatter +Difficulty: Medium +Objective: Format phone numbers to standard format +*/ +// Requirements: +// [x] - Accept 10 digits in any format +// [x] - Output: (XXX) XXX-XXXX +// [x] - Remove all non-digit characters first +// [x] - Return null if not exactly 10 digits + +/** + * Function that format phone numbers to standard format. + * @param {String} phone 10 digits number in any format. + * @returns {String} return phone number formatter. + */ +function formatPhoneNumber(phone) { + const phoneNumber = removeNonDigit(phone) + if (phoneNumber.length === 10) { + // Accept 10 digits in any format + return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6)}` + } else { + return null + } +} +/** + * Function that remove all non-digit characters in phone number. + * @param {String} phone digits number in any format. + * @returns {String} return phone number with non-digit characters. + */ +function removeNonDigit(phone) { + const numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + let phoneDigit = [] + for (const digit of phone) { + if (numbers.includes(digit)) phoneDigit.push(digit) + } + return phoneDigit.join("") +} + +// Test cases: +console.log(`Exercise 12: Phone Number Formatter`) +console.log(formatPhoneNumber("1234567890")) // "(123) 456-7890" +console.log(formatPhoneNumber("123-456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("(123) 456-7890")) // "(123) 456-7890" +console.log(formatPhoneNumber("12345")) // null +console.log(formatPhoneNumber("{123}456}789 0 3365")) // null +console.log("\n==================================\n") + +/* +Exercise 13: Title Case Converter (Advanced) +Difficulty: Hard +Objective: Convert to title case with English grammar rules +*/ +// Requirements: +// [x] - Capitalize first and last word always +// [x] - Capitalize all words except: a, an, the, and, but, or, for, nor, on, at, to, by, in +// [x] - Articles after colon (:) should be capitalized +// [x] - "the quick brown fox" → "The Quick Brown Fox" +// [x] - "a tale of two cities" → "A Tale of Two Cities" + +/** + * Function that convert to title case with English grammar rules. + * @param {String} text any sentence in English. + * @returns {String} a title case sentence with English grammar rules. + */ +function toTitleCase(text) { + const textArray = text.trim().toLowerCase().split(" ") + const wordExcepts = ["a", "an", "of", "the", "and", "but", "or", "for", "nor", "on", "at", "to", "by", "in"] + if (textArray.length > 1) { + // some words + const intermediateWords = [] + const firstWordCapitalized = toCapitalize(textArray[0]) + const lastWordCapitalized = toCapitalize(textArray[textArray.length - 1]) + if (textArray.length === 2) { + return `${firstWordCapitalized} ${lastWordCapitalized}` + } else { + for (let index = 1; index < textArray.length - 1; index++) { + if (!wordExcepts.includes(textArray[index])) { + intermediateWords.push(toCapitalize(textArray[index])) + } else { + intermediateWords.push(textArray[index]) + } + } + return `${firstWordCapitalized} ${intermediateWords.join(" ")} ${lastWordCapitalized}` + } + } else { + //single word + return toCapitalize(textArray[0]) + } +} + +/** + * Function that capitalze a word. + * @param {String} word any lowercase word. + * @returns {String} a capitalized word. + */ +function toCapitalize(word) { + if (word.length > 1) { + const firstLetter = word[0].toUpperCase() + const wordSliced = word.slice(1) + return firstLetter + wordSliced + } else { + return word.toUpperCase() + } +} + +// Test cases: +console.log(`Exercise 13: Title Case Converter (Advanced)`) +console.log(toTitleCase("the lord of the rings")) +// "The Lord of the Rings" +console.log(toTitleCase("a tale of two cities")) +// "A Tale of Two Cities" +console.log(toTitleCase("thE an THE")) +// "The an The" +console.log(toTitleCase("javascript: the gOOd parts")) +// "JavaScript: The Good Parts" +console.log(toTitleCase(" javascript ")) +// "Javascript" +console.log(toTitleCase(" Array sentence: just finished an amazing dayx. ")) +console.log("\n==================================\n") + +/* +Exercise 15: Character Replacer +Difficulty: Medium +Objective: Replace characters based on a mapping object +*/ +// Requirements: +// [x] - Accept string and object mapping +// [x] - Replace all occurrences +// [x] - Case sensitive +// [x] - { 'a': '@', 'e': '3', 'i': '!', 'o': '0' } +// [x] - "hello" → "h3ll0" + +/** + * Function that replace characters based on a mapping object. + * @param {String} str any sentence. + * @param {Object} mapping mapping object to by applied. + */ +function replaceChars(str, mapping) { + const strArray = [] + for (let index = 0; index < str.length; index++) { + if (mapping[str[index]] !== undefined) { + //replace chars + strArray.push(mapping[str[index]]) + } else { + //donn't replace + strArray.push(str[index]) + } + } + return strArray.join("") +} + +// Test cases: +console.log(`Exercise 15: Character Replacer`) +const leetSpeak = { a: "@", e: "3", i: "!", o: "0", s: "$" } +const mappingSpeak = { a: "4", e: "3", i: "1", o: "0", s: "5", u: "v" } +console.log(replaceChars("hello world", leetSpeak)) // "h3ll0 w0rld" +console.log(replaceChars("awesome", leetSpeak)) // "@w3$0m3" +console.log(replaceChars("Welcome to the paradise", mappingSpeak)) // "W3lc0m3 t0 th3 p4r4d153" +console.log(replaceChars("Javascript is awesome. Happy[] coding. Have fun.", mappingSpeak)) // "J4v45cr1pt 15 4w350m3. H4ppy c0d1ng. H4v3 fvn." +console.log("\n==================================\n") + +/* +Exercise 16: URL Parameter Extractor +Difficulty: Hard +Objective: Extract query parameters from URL +*/ +// Requirements: +// [x] - Parse URL query string into object +// [x] - Handle multiple parameters +// [x] - Handle parameters without values +// [x] - "?name=john&age=30&active" → { name: "john", age: "30", active: "" } + +/** + * Function that extract query parameters from URL. + * @param {String} url any url with parameters. + * @returns {Object} obj with parameters from URL. Parameter with value: key: "value" or parameter without values: key: "" + */ +function extractParams(url) { + const urlParameters = url.split("?")[1].split("&") + let queryParameters = {} + for (const query of urlParameters) { + let token = query.split("=") + if (token.length === 1) { + // parameter without values + queryParameters[token[0]] = "" + } else { + // parameter with values + queryParameters[token[0]] = `${token[1]}` + } + } + return queryParameters +} + +// Test cases: +console.log(`Exercise 16: URL Parameter Extractor`) +console.log(extractParams("https://example.com/page?name=john&age=30")) +// { name: "john", age: "30" } + +console.log(extractParams("https://example.com/search?q=javascript&lang=en&sort")) +// { q: "javascript", lang: "en", sort: "" } + +// Case 1 +console.log(extractParams("https://example.com/search?search=javascript%20tutorial&page=1&sort=&filter=")) +// { search: "javascript%20tutorial", page: "1", sort: "", filter: "" } + +// Case 2 +console.log(extractParams("https://example.com/page?user=admin&logged=true&token=abc123&remember")) +// { user: "admin", logged: "true", token: "abc123", remember: "" } + +// Case 3 +console.log(extractParams("https://example.com/page?email=user@example.com&tags=js&tags=react&premium=&redirect=/dashboard")) +// { email: "user@example.com", tags: "js", premium: "", redirect: "/dashboard" } +console.log("\n==================================\n") + +/* +Exercise 17: Hashtag Extractor +Difficulty: Medium +Objective: Find all hashtags in text +*/ +// Requirements: +// [x] - Find all words starting with # +// [x] - Return array of hashtags (without #) +// [x] - No duplicates +// [x] - Case insensitive comparison +// [x] - "#JavaScript is awesome #javascript #WebDev" → ["javascript", "webdev"] + +/** + * Function that find all hashtags in text. + * @param {String} text any sentence. + * @returns {Array} return an array with all hashtags in text. + */ +function extractHashtags(text) { + const sentence = text.toLowerCase().split(" ") + let sentenceSet = new Set() + for (const word of sentence) { + if (word[0] === "#") sentenceSet.add(word.slice(1)) + } + return Array.from(sentenceSet) +} + +// Test cases: +console.log("Exercise 17: Hashtag Extractor") +console.log(extractHashtags("#JavaScript is awesome #javascript #WebDev")) +// ["javascript", "webdev"] +console.log(extractHashtags("No hashtags here")) +// [] +console.log( + extractHashtags( + "Just finished an amazing coding session! Built a password validator from scratch using JavaScript. The feeling when your code finally works perfectly is unbeatable. Time to celebrate with some coffee! #coding #javascript #webdevelopment #programming #developerlife #coffeecode", + ), +) +// ["coding", "javascript "webdevelopment", "programming", "developerlife", "coffeecode"] +console.log( + extractHashtags( + "Exploring the beautiful mountains this weekend was exactly what I needed. Fresh air, stunning views, and complete disconnection from #technology. Nature has a way of resetting your mind and soul. Can't wait for the next adventure! #hiking #nature #mountains #adventure #outdoorlife #weekendvibes #naturelover #TECHNOLOGY", + ), +) +// ["hiking", "nature", "mountains", "adventure", "outdoorlife", "weekendvibes", "naturelover", "TECHNOLOGY"] +console.log( + extractHashtags( + "Finally tried that new Italian restaurant downtown and wow, the pasta was incredible! Homemade fettuccine with truffle sauce that melted in my mouth. The tiramisu for dessert was the perfect ending. Highly recommend it to all food lovers! #foodie #italianfood #pasta #restaurant #foodlover #delicious #foodphotography", + ), +) +// [#foodie", #italianfood", #pasta", #restaurant", #foodlover", #delicious", #foodphotography] +console.log("\n==================================\n") + +/* +Exercise 18: Email Extractor +Difficulty: Medium +Objective: Find all email addresses in text +*/ +// Requirements: +// [x] - Extract all valid-looking emails +// [x] - Return array +// [x] - Basic validation: word@word.word pattern + +/** + * Function that find all email addresses in text and extract it. + * @param {String} text any sentence with email. + * @returns {Array} return any[] valid-looking emails. + */ +function extractEmails(text) { + const sentence = text.trim().toLowerCase().split(" ") + const re = /[a-z]@[a-z].[a-z]/ + let emails = [] + for (const str of sentence) { + if (re.test(str) === true) { + // Test regExp + emails.push(str) + } + } + return emails +} + +// Test cases: +console.log("Exercise 18: Email Extractor") +console.log(extractEmails("Contact us at support@example.com or sales@example.com")) +// ["support@example.com", "sales@example.com"] + +// Case 2: Email with numbers and domains +console.log(extractEmails("Send reports to admin123@mail.company.co.uk and backup to dev@test-server.io")) +// ["admin123@mail.company.co.uk", "dev@test-server.io"] + +// Case 3: Text without emails +console.log(extractEmails("No valid emails here: @example.com, user@, incomplete@")) +// [] + +// Case 4: Only one email +console.log(extractEmails("Hello! Reach me at john.doe@gmail.com, or call me. Thanks!")) +// ["john.doe@gmail.com"] + +// Case 5: Múltiples emails +console.log(extractEmails("Team: alice_smith@company.org, bob-jones@startup.tech, info@support.net and ceo@business.com.")) +// ["alice_smith@company.org", "bob-jones@startup.tech", "info@support.net", "ceo@business.com"] +console.log("\n==================================\n") + +/* +Exercise 19: Find Longest Word +Difficulty: Easy +Objective: Return longest word and its length +*/ +// Requirements: +// [x] - Ignore punctuation +// [x] - Return object: { word: "longest", length: 7 } +// [x] - If tie, return first occurrence + +/** + * Function that find longest word in sentence. + * @param {String} sentence any text. + * @returns {Object} return longest word and its length. + */ +function findLongestWord(sentence) { + const sentenceArray = removePunctuations(sentence.trim()).split(" ") + let longestWord = { word: "", length: 0 } + for (const word of sentenceArray) { + if (longestWord.length < word.length) { + longestWord.word = word + longestWord.length = word.length + } + } + return longestWord +} + +// Test cases: +console.log("Exercise 19: Find Longest Word") +console.log(findLongestWord("The quick brown fox jumps")) +// { word: "quick", length: 5 } or { word: "brown", length: 5 } or { word: "jumps", length: 5 } +console.log(findLongestWord("JavaScript is amazing!")) +// { word: "JavaScript", length: 10 } + +// Case 1 +console.log(findLongestWord("The extraordinary development environment provides unbelievable performance")) +// { word: "extraordinary", length: 13 } + +// Case 2 +console.log(findLongestWord("It's a well-documented state-of-the-art framework")) +// { word: "documented", length: 10x } + +// Case 3 +console.log(findLongestWord("Supercalifragilisticexpialidocious")) +// { word: "Supercalifragilisticexpialidocious", length: 34 } + +// Case 4: Empty +console.log(findLongestWord(" ")) +// { word: "", length: 0 } + +// Case 5 (Bonus) +console.log(findLongestWord("Code123 is shorter than programming456 today!!!")) +// { word: "programming456", length: 14 } +console.log("\n==================================\n") + +/* +Exercise 20: Text Between Delimiters +Difficulty: Medium +Objective: Extract text between two markers +*/ +// Requirements: +// [x] - Extract all text between start and end markers +// [x] - Return array +// [x] - "Hello [name], welcome to [place]" with markers "[" and "]" +// → ["name", "place"] + +/** + * Function that extract text between two markers. + * @param {String} text any sentence. + * @param {String} start initial marker. + * @param {String} end final marker. + * @returns {String} return text between start and end markers. + */ +function extractBetween(text, start, end) { + const escapedStart = escapeRegExp(start) + const escapedEnd = escapeRegExp(end) + const searchTerm = `${escapedStart}([^${escapedEnd}]+)${escapedEnd}` + const regex = new RegExp(searchTerm, "g") + const matches = [] + let match + while ((match = regex.exec(text)) !== null) { + matches.push(match[1]) + } + + return matches +} + +/** + * Function that escape word. + * @param {String} str string sentence. + * @returns string replaced. + */ +function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +// Test cases: +console.log("Exercise 20: Text Between Delimiters") +console.log(extractBetween("Hello [name], welcome to [place]", "[", "]")) +// ["name", "place"] +console.log(extractBetween("The {quick} brown {fox}", "{", "}")) +// ["quick", "fox"] + +console.log(extractBetween("I love {JS} language in {VisualStudio Code}", "{", "}")) +console.log("\n==================================\n") diff --git a/fullstack/javascript/1-variables-strings/exercises/string-exam-solutions.js b/fullstack/javascript/1-variables-strings/exercises/string-exam-solutions.js new file mode 100644 index 0000000..4857116 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/string-exam-solutions.js @@ -0,0 +1,618 @@ +/* +======================================== +ADVANCED STRING MASTERY EXAM — SOLUTIONS +======================================== +Student: Yoandy Doble Herrera +Graded by: Claude +Score: 77/100 (PASS) +======================================== + +PART 1: THEORETICAL KNOWLEDGE (30 points) +PART 2: CODING CHALLENGES (50 points) +PART 3: REAL-WORLD SCENARIOS (20 points) +======================================== +*/ + +// ======================================== +// PART 1: THEORETICAL KNOWLEDGE +// ======================================== + +/* +Q1 — String Immutability (3/3) ✓ +Your answer was correct. +Strings are immutable: once created, the value cannot be changed. +Indexing assignment like str[0] = "H" silently fails. +To "change" a string, you must create a new one. +*/ +let str = "hello" +str[0] = "H" +console.log(str) // "hello" — unchanged + +const newStr = str[0].toUpperCase() + str.slice(1) +console.log("Capitalized:", newStr) // "Hello" + +/* +Q2 — slice vs substring vs substr (2/3) ⚠ +CORRECTION: substr() is the deprecated method, NOT substring(). +- substring(start, end): standard, swaps args if start > end, ignores negatives +- slice(start, end): handles negative indices (counts from end) +- substr(start, length): DEPRECATED — second arg means "length", not "end index" +*/ +const text = "JavaScript" +console.log("\n=== Q2: Slice vs Substring ===") +console.log(text.slice(0, 4)) // "Java" +console.log(text.slice(-6)) // "Script" +console.log(text.substring(0, 4)) // "Java" +console.log(text.substring(4, 0)) // "Java" — substring swaps reversed args + +// substr() — deprecated, avoid in new code: +// console.log(text.substr(4, 6)) // "Script" (start, LENGTH not end) + +/* +Q3 — String Comparison & Unicode (2/3) ⚠ +CORRECTION on "10" < "9": +JS compares strings char-by-char from left to right. +"10" vs "9": first chars are "1"(charCode 49) vs "9"(charCode 57). +Since 49 < 57, the result is true immediately — the "0" never matters. +This is NOT "value coercion" — it's lexicographic (dictionary) ordering. +*/ +console.log("\n=== Q3: String Comparison ===") +console.log("apple" < "banana") // true — 'a'(97) < 'b'(98) +console.log("Apple" < "banana") // true — 'A'(65) < 'b'(98), uppercase is lower charCode +console.log("10" < "9") // true — '1'(49) < '9'(57), stops at first char + +// Proper case-insensitive comparison: +console.log("Apple".toLowerCase() === "apple".toLowerCase()) // true +console.log("apple".localeCompare("Apple", undefined, { sensitivity: "base" })) // 0 (equal) + +// Proper numeric string comparison: +console.log(Number("10") < Number("9")) // false — correct numeric result + +/* +Q4 — Template Literals vs Concatenation (3/3) ✓ +Template literals (Option B) are preferred for readability and +create fewer intermediate string allocations in complex expressions. +*/ +const name = "John" +const age = 30 +const message1 = "Hello, " + name + "! You are " + age + " years old." +const message2 = `Hello, ${name}! You are ${age} years old.` +console.log("\n=== Q4: Template Literals ===") +console.log(message1) +console.log(message2) + +/* +Q5 — Regular Expression Flags (2/3) ⚠ +CORRECTION: Your flag explanations were missing. Here they are: +- /g global: find ALL matches, not just the first +- /i case insensitive: ignores upper/lowercase differences +- /m multiline: ^ and $ match start/end of each LINE, not just the whole string +*/ +const text5 = "Hello HELLO hello" +console.log("\n=== Q5: RegExp Flags ===") +console.log(text5.match(/hello/)) // ["hello", index:12, ...] — first match only +console.log(text5.match(/hello/g)) // ["hello"] — all lowercase matches +console.log(text5.match(/hello/gi)) // ["Hello","HELLO","hello"] — all, any case + +// /m flag example: +const multiline = "start of line1\nstart of line2" +console.log(multiline.match(/^start/gm)) // ["start","start"] — matches each line start + +/* +Q6 — String Method Side Effects (1/3) ✗ +CORRECTION: NO string method modifies the original string — EVER. +Strings are immutable (as you correctly said in Q1!). +Every method returns a NEW value; the original is always untouched. +*/ +const str6 = " hello world " +console.log("\n=== Q6: String Methods — None Mutate the Original ===") + +str6.trim() // returns new string — str6 unchanged +str6.toUpperCase() // returns new string — str6 unchanged +str6.replace("o", "0") // returns new string — str6 unchanged +str6.split(" ") // returns new array — str6 unchanged + +console.log("Original after all methods:", `"${str6}"`) // " hello world " — untouched + +// To use the results you MUST capture them: +const trimmed = str6.trim() +const upper = str6.toUpperCase() +const replaced = str6.replace("o", "0") +const splitArr = str6.split(" ") +console.log(trimmed) // "hello world" +console.log(upper) // " HELLO WORLD " +console.log(replaced) // " hell0 world " +console.log(splitArr) // ["", "hello", "world", ""] + +/* +Q7 — Performance in Loops (3/3) ✓ +Array join (Approach B) is faster for large string building. +In Approach A, each += creates a new string object (10,000 allocations). +In Approach B, the array accumulates references cheaply, then one join creates the string. +*/ +// Approach A — slow for large N: +let resultA = "" +for (let i = 0; i < 10000; i++) { + resultA += "text" +} + +// Approach B — fast: +const parts = [] +for (let i = 0; i < 10000; i++) { + parts.push("text") +} +const resultB = parts.join("") + +// Modern alternative — also fast and readable: +const resultC = Array.from({ length: 10000 }, () => "text").join("") + +/* +Q8 — Character Encoding (2/3) ⚠ +CORRECTIONS: +- charCodeAt() returns a Number (104), not the string "104" +- "caf茅".length is 5, not 4 — because 茅 is a surrogate pair (2 UTF-16 code units) +- JavaScript uses UTF-16 internally (not UTF-8) +*/ +console.log("\n=== Q8: Character Encoding ===") +console.log("hello".length) // 5 +console.log("hello".charCodeAt(0)) // 104 (a Number, not a string) +console.log(String.fromCharCode(72)) // "H" + +console.log("caf茅".length) // 5, not 4! — 茅 takes 2 code units +console.log([..."caf茅"].length) // 4 — spread gives correct visual character count +console.log("茅".charCodeAt(0)) // 55356 — high surrogate +console.log("茅".charCodeAt(1)) // 57012 — low surrogate +console.log("茅".codePointAt(0)) // 127829 — full Unicode code point + +/* +Q9 — Search Method Speed Ranking (1/3) ✗ +CORRECTION — correct ranking fastest to slowest: +1. indexOf() — raw linear scan, returns index, minimal overhead +2. includes() — same scan as indexOf internally, returns boolean +3. search() — compiles RegExp engine, finds first match +4. match() — RegExp engine + allocates result array with metadata +*/ +const text9 = "The quick brown fox" +console.log("\n=== Q9: Search Methods ===") +console.log(text9.indexOf("quick")) // 4 — fastest +console.log(text9.includes("quick")) // true — nearly as fast as indexOf +console.log(text9.search(/quick/)) // 4 — regex overhead +console.log(text9.match(/quick/)) // ["quick", index:4, ...] — slowest, builds array + +/* +Q10 — Common Pitfalls (3/3) ✓ +Your email fix and one-liner reversal were correct. +*/ +console.log("\n=== Q10: Common Pitfalls ===") + +// Fixed email validator: +const fixedVersion = function (email) { + const emailSplitted = email.split("@") + if (emailSplitted.length !== 2) return "Invalid email" + const [beforeAt, afterAt] = emailSplitted + if (beforeAt.length <= 0) return "Invalid email" + if (!afterAt.includes(".") || afterAt.length === 0) return "Invalid email" + return "Valid email" +} +console.log(fixedVersion("user@example.com")) // "Valid email" +console.log(fixedVersion("user@@example.com")) // "Invalid email" +console.log(fixedVersion("@example.com")) // "Invalid email" + +// One-liner reversal: +const reversed = "Hello World".split(" ").reverse().join(" ") +console.log(reversed) // "World Hello" + +// ======================================== +// PART 2: CODING CHALLENGES +// ======================================== + +/* +Challenge 1 — String Compression (4/5) ⚠ +BUG in your solution: you used an object to count ALL occurrences globally, +so "abac" → "a2b1c1" instead of "abac" (runs of 'a' aren't consecutive). +FIX: count consecutive runs only (walk the string linearly). +*/ +console.log("\n=== Challenge 1: String Compression ===") + +function compressString(str) { + if (!str) return str + + let result = "" + let i = 0 + + while (i < str.length) { + const char = str[i] + let count = 0 + // count consecutive identical characters + while (i < str.length && str[i] === char) { + count++ + i++ + } + // only append count if > 1 (keeps "abc" as "abc" not "a1b1c1") + result += count > 1 ? char + count : char + } + + // only return compressed form if it's actually shorter + return result.length < str.length ? result : str +} + +console.log(compressString("aaabbc")) // "a3b2c" +console.log(compressString("abc")) // "abc" +console.log(compressString("aabbccdd")) // "a2b2c2d2" +console.log(compressString("aaaaaaaaaa")) // "a10" +console.log(compressString("")) // "" + +/* +Challenge 2 — Anagram Checker (7/7) ✓ +Your solution was clean and correct. Reproduced as-is. +*/ +console.log("\n=== Challenge 2: Anagram Checker ===") + +function areAnagrams(str1, str2) { + const sanitize = (s) => + s + .trim() + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + const s1 = sanitize(str1) + const s2 = sanitize(str2) + + if (s1.length !== s2.length) return false + + const counts = new Map() + for (const char of s1) { + counts.set(char, (counts.get(char) || 0) + 1) + } + for (const char of s2) { + if (!counts.has(char)) return false + counts.set(char, counts.get(char) - 1) + if (counts.get(char) < 0) return false + } + + return true +} + +console.log(areAnagrams("listen", "silent")) // true +console.log(areAnagrams("Hello", "world")) // false +console.log(areAnagrams("The eyes", "They see")) // true +console.log(areAnagrams("Dormitory", "Dirty room")) // true +console.log(areAnagrams("abc", "abcd")) // false + +/* +Challenge 3 — Custom Template Engine (7/8) ✓ +Your solution works. Simplified here — the inner regex is redundant +since .replace() already captures the match. +*/ +console.log("\n=== Challenge 3: Template Engine ===") + +function processTemplate(template, data) { + return template.replace(/{{(.+?)}}/g, (match, key) => { + // key is the captured group (the variable name, trimmed) + return key in data ? data[key] : match + }) +} + +console.log(processTemplate("Hello {{name}}!", { name: "John" })) +// "Hello John!" + +console.log( + processTemplate("{{greeting}} {{name}}, you have {{count}} messages", { + greeting: "Hi", + name: "Alice", + count: 5, + }), +) +// "Hi Alice, you have 5 messages" + +console.log(processTemplate("No variables here", {})) +// "No variables here" + +console.log(processTemplate("{{missing}} variable", { name: "test" })) +// "{{missing}} variable" + +/* +Challenge 4 — Permutations (4/8) ✗ → Fixed +Your approach generated some permutations but not all. +FIX: use recursion — pick each character as "first", permute the rest. +Key insight: permutations(n) = n × permutations(n-1) +*/ +console.log("\n=== Challenge 4: Permutations ===") + +function getPermutations(str) { + // base cases + if (str === "" || str === " ") return [str] + if (str.length === 1) return [str] + + const result = [] + + for (let i = 0; i < str.length; i++) { + const first = str[i] + // all characters except position i + const rest = str.slice(0, i) + str.slice(i + 1) + // recursively get all permutations of the remainder + const restPerms = getPermutations(rest) + // prepend current char to each sub-permutation + for (const perm of restPerms) { + result.push(first + perm) + } + } + + return result +} + +console.log(getPermutations("abc")) +// ["abc", "acb", "bac", "bca", "cab", "cba"] + +console.log(getPermutations("ab")) +// ["ab", "ba"] + +console.log(getPermutations("a")) +// ["a"] + +console.log(getPermutations("")) +// [""] + +/* +Challenge 5 — Balanced Brackets (4/7) ✗ → Fixed +Your mirror-pair approach fails for nested/interleaved brackets like "([{}])". +FIX: use a stack — push openers, pop and verify on closers. +This is the canonical solution and works for any nesting depth. +*/ +console.log("\n=== Challenge 5: Balanced Brackets ===") + +function hasBalancedBrackets(str) { + if (!str.trim()) return true + + const stack = [] + const pairs = { ")": "(", "]": "[", "}": "{" } + const openers = new Set(["(", "[", "{"]) + + for (const char of str) { + if (openers.has(char)) { + stack.push(char) // push every opening bracket + } else if (pairs[char]) { + // closing bracket: top of stack must be its matching opener + if (stack.pop() !== pairs[char]) { + return false + } + } + } + + return stack.length === 0 // stack must be fully empty at end +} + +console.log(hasBalancedBrackets("()")) // true +console.log(hasBalancedBrackets("()[]{}")) // true +console.log(hasBalancedBrackets("(]")) // false +console.log(hasBalancedBrackets("([{}])")) // true +console.log(hasBalancedBrackets("[(])")) // false +console.log(hasBalancedBrackets("{[}")) // false +console.log(hasBalancedBrackets("")) // true + +/* +Challenge 6 — Roman Numerals (3/7) ✗ → Fixed +Your solution was incomplete (worked for 1–9 only, stubs for everything else). +FIX: build a lookup table that includes ALL subtractive pairs (CM, CD, XC, XL, IX, IV), +then use a greedy loop — subtract the largest value that fits, repeat. +*/ +console.log("\n=== Challenge 6: Roman Numerals ===") + +function toRomanNumeral(num) { + if (num < 1 || num > 3999) return "Out of range (1–3999)" + + // All values including subtractive pairs, descending order + const lookup = [ + [1000, "M"], + [900, "CM"], + [500, "D"], + [400, "CD"], + [100, "C"], + [90, "XC"], + [50, "L"], + [40, "XL"], + [10, "X"], + [9, "IX"], + [5, "V"], + [4, "IV"], + [1, "I"], + ] + + let result = "" + + for (const [value, symbol] of lookup) { + while (num >= value) { + result += symbol + num -= value + } + } + + return result +} + +console.log(toRomanNumeral(3)) // "III" +console.log(toRomanNumeral(4)) // "IV" +console.log(toRomanNumeral(9)) // "IX" +console.log(toRomanNumeral(58)) // "LVIII" +console.log(toRomanNumeral(1994)) // "MCMXCIV" + +/* +Challenge 7 — Run-Length Decoding (7/7) ✓ +Your solution was clean and elegant. Reproduced as-is. +*/ +console.log("\n=== Challenge 7: Run-Length Decoding ===") + +function decodeString(encoded) { + const encodedArray = encoded.match(/[a-z]\d+/g) + if (!encodedArray) return "" + return encodedArray.map((token) => token[0].repeat(Number(token.slice(1)))).join("") +} + +console.log(decodeString("a3b2c1")) // "aaabbc" +console.log(decodeString("a10")) // "aaaaaaaaaa" +console.log(decodeString("x1y1z1")) // "xyz" +console.log(decodeString("")) // "" +console.log(decodeString("m15")) // "mmmmmmmmmmmmmmm" + +// ======================================== +// PART 3: REAL-WORLD SCENARIOS +// ======================================== + +/* +Scenario 1 — Input Sanitization / XSS Prevention (6/7) ✓ +Your solution works. Minor issue: the ternary inside map() didn't reassign +correctly — the return char on the next line saved it, but it's a hidden bug. +FIX: make the map callback explicit and clean. +*/ +console.log("\n=== Scenario 1: Input Sanitization ===") + +function sanitizeComment(input) { + const htmlMap = new Map([ + ["<", "<"], + [">", ">"], + ["&", "&"], + ["'", "'"], + ['"', """], + ]) + + // 1. Trim whitespace + // 2. Remove Hello")) +// "Hello" + +console.log(sanitizeComment("Bold text")) +// "<b>Bold text</b>" + +console.log(sanitizeComment(" Normal comment ")) +// "Normal comment" + +console.log(sanitizeComment("Quote: 'test' and \"test\"")) +// "Quote: 'test' and "test"" + +/* +Scenario 2 — Query String Builder (7/7) ✓ +Your solution was well-structured and correct. +One improvement: use encodeURIComponent() for robustness (handles &, =, #, etc.) +instead of only replacing spaces with %20. +*/ +console.log("\n=== Scenario 2: Query String Builder ===") + +function buildQueryString(params) { + const parts = [] + + for (const [key, value] of Object.entries(params)) { + if (value === null || value === undefined) continue + + if (Array.isArray(value)) { + // arrays: tags=javascript&tags=coding + for (const element of value) { + if (typeof element === "string" || typeof element === "number") { + parts.push(`${key}=${encodeURIComponent(element)}`) + } + } + } else if (typeof value === "object") { + // nested objects: user[id]=123&user[name]=Alice + for (const [subKey, subVal] of Object.entries(value)) { + if (typeof subVal === "string" || typeof subVal === "number") { + parts.push(`${key}[${subKey}]=${encodeURIComponent(subVal)}`) + } + } + } else if (typeof value === "string" || typeof value === "number") { + parts.push(`${key}=${encodeURIComponent(value)}`) + } + } + + return parts.join("&") +} + +console.log(buildQueryString({ name: "John", age: 30 })) +// "name=John&age=30" + +console.log(buildQueryString({ search: "hello world", page: 1 })) +// "search=hello%20world&page=1" + +console.log(buildQueryString({ tags: ["javascript", "coding"] })) +// "tags=javascript&tags=coding" + +console.log(buildQueryString({ user: { id: 123, name: "Alice" } })) +// "user[id]=123&user[name]=Alice" + +console.log(buildQueryString({ a: "test", b: null, c: undefined, d: "value" })) +// "a=test&d=value" + +/* +Scenario 3 — Efficient String Search (5/6) ✓ +Your solution was correct and well-structured. exec() in a while loop +with a persistent RegExp is exactly the right pattern for global matches. +Minor cleanup: extracted theContext() logic, simplified edge case handling. +*/ +console.log("\n=== Scenario 3: Efficient String Search ===") + +function findAllOccurrences(text, terms) { + const results = [] + + for (const term of terms) { + const regex = new RegExp(term, "g") + let match + + while ((match = regex.exec(text)) !== null) { + const pos = match.index + const before = text.slice(Math.max(0, pos - 10), pos) + const after = text.slice(pos + term.length, pos + term.length + 10) + + const context = (pos - 10 > 0 ? "..." : "") + before + term + after + (pos + term.length + 10 < text.length ? "..." : "") + + results.push({ term, position: pos, context }) + } + } + + return results.sort((a, b) => a.position - b.position) +} + +const document = "JavaScript is a programming language. JavaScript is widely used." +const searchTerms = ["JavaScript", "language"] + +console.log(findAllOccurrences(document, searchTerms)) +// [ +// { term: "JavaScript", position: 0, context: "JavaScript is a pr..." }, +// { term: "language", position: 30, context: "...mming language. JavaScr..." }, +// { term: "JavaScript", position: 39, context: "...uage. JavaScript is wide..." } +// ] + +// ======================================== +// END OF SOLUTIONS +// ======================================== + +/* +KEY TAKEAWAYS: +───────────────────────────────────────── +1. String immutability — no method ever modifies the original. + Every method returns a NEW value. Always. + +2. substr() is deprecated. Use slice() or substring() instead. + +3. String comparison is lexicographic (char-by-char), not numeric. + "10" < "9" because '1' < '9' by charCode, not because 10 < 9. + +4. Stack pattern — the right tool for balanced brackets (and many parsing problems). + +5. Recursion — the right tool for permutations: pick one, permute the rest. + +6. Greedy lookup table — the right tool for Roman numerals and coin-change problems. + +7. Surrogate pairs — "caf茅".length === 5. Use [...str].length for visual char count. +───────────────────────────────────────── +*/ diff --git a/fullstack/javascript/1-variables-strings/exercises/string-exam.js b/fullstack/javascript/1-variables-strings/exercises/string-exam.js new file mode 100644 index 0000000..73e941a --- /dev/null +++ b/fullstack/javascript/1-variables-strings/exercises/string-exam.js @@ -0,0 +1,888 @@ +/* +======================================== +ADVANCED STRING MASTERY EXAM +======================================== +Student: Yoandy Doble Herrera +Date: 09/03/2026 +Start Time: 07:15 am +End Time: 11:03 pm +======================================== + +INSTRUCTIONS: +1. Answer all questions in this file +2. Show your work - add comments explaining your logic +3. Test your code - make sure it runs without errors +4. Time Limit: 90 minutes +5. Passing Score: 75/100 + +GRADING: +- Part 1 (Theory): 30 points +- Part 2 (Coding): 50 points +- Part 3 (Scenarios): 20 points +- Total: 100 points + +When finished, save this file and submit it for grading. +Good luck! +======================================== +*/ + +// ======================================== +// PART 1: THEORETICAL KNOWLEDGE (30 points) +// ======================================== + +/* +Question 1 (3 points): String Immutability +Explain string immutability in JavaScript. +*/ + +// Given this code: +let str = "hello" +str[0] = "H" +console.log(str) // What is the output and why? + +// Your Answer: The console.log output is: hello. +// Output: hello +/* Explanation: Strings are inmutables. Once creates an string it can't be changed. Only reasign to itself or concat to other string you can modify strings. +eg. +const newStr = str[0].toUpperCase() + str.slice(1) +console.log("New str:>>> " + newStr) +*/ + +/* +Question 2 (3 points): Slice vs Substring vs Substr +What's the difference between .slice(), .substring(), and .substr()? +*/ + +const text = "JavaScript" + +// Fill in the outputs: +console.log(text.slice(0, 4)) // Output: Java +console.log(text.slice(-6)) // Output: Script +console.log(text.substring(0, 4)) // Output: Java +console.log(text.substring(4, 0)) // Output: Java (note: reversed parameters) + +// Which method is deprecated? +// Answer: Substring is deprecated because not handle negative indices. Substringg is zero-based in the end parameter. + +// Which method handles negative indices? +// Answer: slice() + +/* +Question 3 (3 points): String Comparison and Unicode +*/ + +// What do these comparisons return and why? +console.log("apple" < "banana") // true +console.log("Apple" < "banana") // true +console.log("10" < "9") // true + +// YOUR ANSWERS: +// "apple" < "banana": true +// Explanation: This comparison checks the character unicode, so banana charCode is bigger than apple. This is value coerccion. Javascript tries to compare this type of value. + +// "Apple" < "banana": true +// Explanation: This comparison checks the character unicode, so banana charCode is bigger than Apple. This is value coerccion. Javascript tries to compare this type of value. + +// "10" < "9": true +// Explanation: This comparison checks the character unicode, so banana charCode is bigger than Apple. This is value coerccion. Javascript tries to compare this type of value. + +// How would you properly compare strings ignoring case? +// Your answer: Using localeCompare(). Your excersice shows value coerccion. Javascript tries to compare this type of value. + +/* +Question 4 (3 points): Template Literals vs Concatenation +*/ + +const name = "John" +const age = 30 + +// Which is more efficient for performance? Why? +// Option A: +const message1 = "Hello, " + name + "! You are " + age + " years old." + +// Option B: +const message2 = `Hello, ${name}! You are ${age} years old.` + +// YOUR ANSWER: Option B +// More efficient: Option B +// Reason: Template literals are more efficient for performance because the variables are invoqued inside the template. You have only one string using literals and when you use concat you are adding several string to create one. + +/* +Question 5 (3 points): Regular Expression Flags +*/ + +// Explain what each flag does: +// /pattern/g // g flag: +// /pattern/i // i flag: +// /pattern/m // m flag: + +// What's the output of this code? +const text5 = "Hello HELLO hello" +console.log(text5.match(/hello/)) // Output: [ 'hello', index: 12, input: 'Hello HELLO hello', groups: undefined ] +console.log(text5.match(/hello/g)) // Output: [ 'hello' ] +console.log(text5.match(/hello/gi)) // Output: [ 'Hello', 'HELLO', 'hello' ] + +// YOUR ANSWERS: +// /hello/ output: [hello] regExp search in string a pattern. Returns first value founded. +// /hello/g output: [hello] regExp search in string a pattern. Returns all values founded. Global flag. +// /hello/gi output: [Hello, HELLO, hello] regExp search in string a pattern. Returns all values founded. Global and case insensitive flag. + +/* +Question 6 (3 points): String Method Side Effects +*/ + +// Which of these methods modify the original string? +const str6 = " hello world " + +str6.trim() // Modifies original? No +str6.toUpperCase() // Modifies original? Yes +str6.replace("o", "0") // Modifies original? Yes +str6.split(" ") // Modifies original? No + +console.log(">>>Trim", str6.trim()) +console.log(">>>toUpperCase", str6.toUpperCase()) +console.log(">>>Replace", str6.replace("o", "0")) +console.log(">>>Split", str6.split(" ")) + +// YOUR ANSWERS: I use base case for each str6. I infered every method you test. +// trim(): No, It don't modifies original. "hello world" Remove start and end white space in string. +// toUpperCase(): Yes, "HELLO WORLD" Convert string to upper case letters. It modifies original. +// replace(): Yes, " hell0 world " Replace first ocurrency in search replace. It changes value to: 0. It modifies original. +// split(): No, ["", "hello", " world", ""] Converts string into array splitting by " " . In my opinion don't modifies original. I could apply .join(" ") and obtain same result again. + +/* +Question 7 (3 points): Performance Considerations +*/ + +// Which approach is faster for building a long string? Why? + +// Approach A: Concatenation in loop +let result = "" +for (let i = 0; i < 10000; i++) { + result += "text" +} + +// Approach B: Array join +const parts = [] +for (let i = 0; i < 10000; i++) { + parts.push("text") +} +const resultB = parts.join("") + +// YOUR ANSWER: Approach B +// Faster approach: Array join +// Reason: The option B is faster for building a long string because you build the long string from an array after many operations in (for). In option A, every loop creates a new string in this case 10000 new strings. + +/* +Question 8 (3 points): Character Encoding +*/ + +// What's the difference between these? +"hello".length // Returns: 5 it's the length of a String. +"hello".charCodeAt(0) // Returns: "104" char code from letter "h". Zero-based index from a character. +String.fromCharCode(72) // Returns: "H" letter from char code. + +console.log(">>>Length", "hello".length) +console.log(">>>charCodeAt", "hello".charCodeAt(0)) +console.log(">>>fromCharCode", String.fromCharCode(72)) + +// YOUR ANSWERS: +// .length returns: +// .charCodeAt(0) returns: +// .fromCharCode(72) returns: + +console.log(">>>caf", "caf茅".length) +// What would "caf茅".length return and why might this be surprising? +// Answer: >>> 4. Javascript uses UTF-8 encoding. Every char represents a position in the string index. Also every elements is a char code. + +/* +Question 9 (3 points): String Searching Methods +*/ + +const text9 = "The quick brown fox" + +// Rank these methods by speed (fastest to slowest) and explain why: +text9.indexOf("quick") +text9.includes("quick") +text9.match(/quick/) +text9.search(/quick/) + +// YOUR RANKING (1=fastest, 4=slowest): +// 1. text9.search(/quick/) +// 2. text9.indexOf("quick") +// 3. text9.includes("quick") +// 4. text9.match(/quick/) +// Explanation: Tricky question I'll do the best. +/* + The fastest is search. Finds the first substring from string, makes a loop to search. After I pick indexOf it uses search and break in the first occurrence substring. Later I pick includes it uses search for loop entire String and convert the object to String. Finally match uses a search and creates and array with ocurrences. + */ + +/* +Question 10 (3 points): Common Pitfalls +*/ + +// What's wrong with this code? Fix it. + +// Problem 1: +const emailQ10 = "user@example.com" +if (emailQ10.includes("@") && emailQ10.includes(".")) { + console.log("Valid email") +} +// YOUR ANSWER: +// Issue: Email must contains only one "@" and checks after the "@" if contains "." We can use regExp but this problem shows simple verification. I'm gonna use simple verification to. +// Fixed version: + +const correo = "user@example.com" + +/** + * Problem 1 email checker + * @param {String} email any email + * @returns {Boolean} true or false + */ +const fixedVersion = function (email) { + const emailSplitted = email.split("@") + if (emailSplitted.length !== 2) return "Invalid email" + const [beforeArroba, afterArroba] = emailSplitted + if (beforeArroba.length <= 0) return "Invalid email" + if (!afterArroba.includes(".") && afterArroba.length > 0) { + return "Invalid email" + } else { + return "Valid email" + } +} +fixedVersion(correo) + +/** + * Problem 1 email checker version regExp + * @param {String} email any email + * @returns {Boolean} true or false + */ +const emailCheck = function (email) { + const regExp = /[\w_-]@[\w_-].[a-z]{2,}/ + if (regExp.test(email)) { + console.log("Valid email") + } else { + console.log("Invalid email") + } +} +emailCheck(correo) + +// Problem 2: +const text10 = "Hello World" +const words = text10.split(" ") +const reversed = words.reverse().join(" ") +// Can you do this in one line? Yes, I can concat multiple string methods in one line. Remember you teach me to avoid hardcoding and simplify operations. +// YOUR ANSWER: const reversed = "Hello World".split(" ").reverse().join(" ") + +// ======================================== +// PART 2: CODING CHALLENGES (50 points) +// ======================================== + +/* +Challenge 1 (5 points): String Compression +Compress a string using character counts +"aaabbc" → "a3b2c1" +"abc" → "abc" (no compression if result isn't shorter) +"" → "" +*/ + +/** + * Compress a string using character counts. + * @param {String} str any simple word. + * @returns {String} str string compressed. + */ +function compressString(str) { + let isCompress = false + let strCompress = [] + let charObj = {} + if (str === "") return str + // Create an object with key as letter and value: total char count + str.split("").forEach((char) => (char in charObj ? charObj[char]++ : (charObj[char] = 1))) + // Create an array from Obj an chech if str is compress or not + for (const key in charObj) { + if (charObj[key] > 1) { + strCompress.push(`${key}${charObj[key]}`) + isCompress = true + } else { + strCompress.push(`${key}1`) + } + } + if (isCompress) { + return strCompress.join("") + } else { + return strCompress.join("").replace(/[^a-z]/g, "") + } +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 1: String Compression ===") +console.log(compressString("aaabbc")) // Expected: "a3b2c1" +console.log(compressString("abc")) // Expected: "abc" +console.log(compressString("aabbccdd")) // Expected: "a2b2c2d2" +console.log(compressString("aaaaaaaaaa")) // Expected: "a10" +console.log(compressString("")) // Expected: "" + +/* +Challenge 2 (7 points): Anagram Checker +Check if two strings are anagrams +Ignore spaces, punctuation, and case +*/ + +/** + * Function that check if two strings are anagrams. + * @param {String} str1 any words. + * @param {String} str2 any words. + * @returns {Boolean} true if anagrams otherwise false. + */ +function areAnagrams(str1, str2) { + let decision = true + const strSanitized1 = str1 + .trim() + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + const strSanitized2 = str2 + .trim() + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + // Length must be equals + if (strSanitized1.length !== strSanitized2.length) return false + // Check anagrams same char counts on Map + const str1Map = charCounts(strSanitized1) + const str2Map = charCounts(strSanitized2) + for (let [key, value] of str1Map) { + if (!str2Map.has(key) || str2Map.get(key) !== value) { + decision = false + } + } + return decision +} + +/** + * Function character counts and adds the count into a Map. + * @param {String} str any lowercase string. + * @returns {Map} a Map with the character counts. + */ +const charCounts = function (str) { + let charMap = new Map() + str.split("").forEach((char) => (charMap.has(char) ? charMap.set(char, charMap.get(char) + 1) : charMap.set(char, 1))) + return charMap +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 2: Anagram Checker ===") +console.log(areAnagrams("listen", "silent")) // Expected: true +console.log(areAnagrams("Hello", "world")) // Expected: false +console.log(areAnagrams("The eyes", "They see")) // Expected: true +console.log(areAnagrams("Dormitory", "Dirty room")) // Expected: true +console.log(areAnagrams("abc", "abcd")) // Expected: false + +/* +Challenge 3 (8 points): Custom Template Engine +Simple template engine that replaces {{variable}} with values +*/ + +/** + * Function simple template engine that replaces {{variable}} with values. + * @param {String} template string with {{variable}}. + * @param {Object} data object with the properties variable and value. + * @returns {String} literal string replaced token {{variable}} with values. + */ +function processTemplate(template, data) { + // Replace string using function + let templateReplaced = template.replace(/{{.+?}}/g, function (match) { + // String regular expression + const regExpStr = "{{(.+?)}}" + const reg = new RegExp(regExpStr, "g") + const [matchOrigin, cleanMatch] = reg.exec(match) + return (match = data[cleanMatch] || match) + }) + return templateReplaced +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 3: Template Engine ===") +console.log(processTemplate("Hello {{name}}!", { name: "John" })) +// Expected: "Hello John!" + +console.log( + processTemplate("{{greeting}} {{name}}, you have {{count}} messages", { + greeting: "Hi", + name: "Alice", + count: 5, + }), +) +// Expected: "Hi Alice, you have 5 messages" + +console.log(processTemplate("No variables here", {})) +// Expected: "No variables here" + +console.log(processTemplate("{{missing}} variable", { name: "test" })) +// Expected: "{{missing}} variable" + +/* +Challenge 4 (8 points): Find All Permutations +Generate all permutations of a string +*/ + +/** + * Function that generate all permutations of a string. + * @param {String} str any string word. + * @returns {array} an array with all permutations. + */ +function getPermutations(str) { + let permutations = [] + + // Check empty string + if (str === "" || str === " ") { + return [str] + } + // Check only one letter + if (str.length === 1) { + return [str] + } + // Check two letters + if (str.length === 2) { + // 1- add original str to array + permutations.push(str) + // 2- reverse elements + permutations.push(str.split("").toReversed().join("")) + return permutations + } + + //Permutations + // 1- add original str to array + permutations.push(str) + const strArray = str.split("") + for (let index = 0; index < strArray.length; index++) { + console.log(">>>Index-", index) + // 2- permut first element -element + reversed slice elements- + if (index === 0) { + permutations.push( + `${strArray[index]}${strArray + .slice(index + 1) + .toReversed() + .join("")}`, + ) + } else { + // Permutation: element + before + after + permutations.push(`${strArray[index]}${strArray.slice(0, index).join("")}${strArray.slice(index + 1).join("")}`) + // Permutation: element + (before + after).reversed + permutations.push( + `${strArray[index]}${strArray + .slice(0, index) + .join("") + .concat(strArray.slice(index + 1).join("")) + .split("") + .reverse() + .join("")}`, + ) + } + } + + return permutations +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 4: Permutations ===") +console.log(getPermutations("abc")) +// Expected: ["abc", "acb", "bac", "bca", "cab", "cba"] + +console.log(getPermutations("ab")) +// Expected: ["ab", "ba"] + +console.log(getPermutations("a")) +// Expected: ["a"] + +console.log(getPermutations("")) +// Expected: [""] + +/* +Challenge 5 (8 points): Balanced Brackets Checker +Check if string has balanced brackets/parentheses +*/ + +/** + * Function that checks if string has balanced brackets/parentheses. + * @param {String} str string with brackets/parentheses. + * @returns {Boolean} true if string has balanced brackets/parentheses otherwise false. + */ +function hasBalancedBrackets(str) { + let decision + // Check str isEmpty + if (str === "" || str === " ") return true + // Check str length isPar so brackets/parentheses could be balanced. + if (str.length % 2 !== 0) return false + // Checks pairs: (), [], {} + const strByPair = str.match(/.{1,2}/g) + decision = hasBalancedBracketsArray(strByPair) + // Check case mirror string split at middle, after create pairs + if (decision === false) { + let strPairArr = [] + let forward = 1 + for (let index = 0; index < str.length / 2; index++) { + strPairArr.push(`${str[index]}${str[str.length - forward]}`) + forward++ + } + decision = hasBalancedBracketsArray(strPairArr) + } + return decision +} + +/** + * Function that checks in array "()", "[]", "{}" + * @param {Array} arr array of tuplas brackets like "()", "[]", "{}" + * @returns {Boolean} true if has balanced brackets otherwise false. + */ +function hasBalancedBracketsArray(arr) { + const validPairs = ["()", "[]", "{}"] + let decision = true + for (const element of arr) { + if (!validPairs.includes(element)) { + decision = false + } + } + return decision +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 5: Balanced Brackets ===") +console.log(hasBalancedBrackets("()")) // Expected: true +console.log(hasBalancedBrackets("()[]{}")) // Expected: true +console.log(hasBalancedBrackets("(]")) // Expected: false +console.log(hasBalancedBrackets("([{}])")) // Expected: true +console.log(hasBalancedBrackets("[(])")) // Expected: false +console.log(hasBalancedBrackets("{[}")) // Expected: false +console.log(hasBalancedBrackets("")) // Expected: true + +/* INCOMPLETE +Challenge 6 (7 points): Roman Numeral Converter +Convert integer to Roman numeral +Range: 1-3999 +*/ + +/** + * Rules + * 1- Symbols writing/reading from ltr. + * 2- Numbers = the sum of values composition. + * 3- Rest value when an element with menor value is next to a bigger value. + * 4- Base 5 number never will rest or repeat. + * 5- Base 10 and I can repeat 3 times. + * @param {Number} num + * @returns {String} + */ +function toRomanNumeral(num) { + // All substractive tokens + const romanAlphabet = new Map([ + [1000, "M"], + [900, "CM"], + [500, "D"], + [400, "CD"], + [100, "C"], + [90, "XC"], + [50, "L"], + [40, "XL"], + [10, "X"], + [9, "IX"], + [5, "V"], + [4, "IV"], + [1, "I"], + ]) + let result = [] + if (num < 1 || num > 3999) return "Number out or range 0 - 3999" + // Check exact number case + if (romanAlphabet.has(num)) { + return romanAlphabet.get(num) + } + for (const [value, token] of romanAlphabet) { + while (num >= value) { + result.push(token) + console.log(">>>Result", result) + num -= value + console.log(">>>Value", value) + } + } + return result.join("") +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 6: Roman Numerals ===") +console.log(toRomanNumeral(3)) // Expected: "III" +console.log(toRomanNumeral(4)) // Expected: "IV" +console.log(toRomanNumeral(9)) // Expected: "IX" +console.log(toRomanNumeral(58)) // Expected: "LVIII" +console.log(toRomanNumeral(1994)) // Expected: "MCMXCIV" + +/* +Challenge 7 (7 points): Run-Length Decoding +Decode a run-length encoded string +"a3b2c1" → "aaabbc" +"a10" → "aaaaaaaaaa" +*/ + +/** + * Function that decode a run-length encoded string. + * @param {String} encoded run-length encoded string. + * @returns {String} a decoded run-length encoded string. + */ +function decodeString(encoded) { + const encodedArray = encoded.match(/[a-z]\d{1,}/g) + if (encodedArray === null) return "" + const decoded = encodedArray + .map((encodedToken) => { + const char = encodedToken[0] + const charLength = encodedToken.slice(1) + return (encodedToken = char.repeat(charLength)) + }) + .join("") + return decoded +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Challenge 7: Run-Length Decoding ===") +console.log(decodeString("a3b2c1")) // Expected: "aaabbc" +console.log(decodeString("a10")) // Expected: "aaaaaaaaaa" +console.log(decodeString("x1y1z1")) // Expected: "xyz" +console.log(decodeString("")) // Expected: "" +console.log(decodeString("m15")) // Expected: "mmmmmmmmmmmmmmm" + +// ======================================== +// PART 3: REAL-WORLD SCENARIOS (20 points) +// ======================================== + +/* +Scenario 1 (7 points): Security - Input Sanitization +You're building a comment system. Sanitize user input to prevent XSS attacks. + +Requirements: +- Remove Hello")) +// Expected: "Hello" + +console.log(sanitizeComment("Bold text")) +// Expected: "<b>Bold text</b>" + +console.log(sanitizeComment(" Normal comment ")) +// Expected: "Normal comment" + +console.log(sanitizeComment("Quote: 'test' and \"test\"")) +// Expected: "Quote: 'test' and "test"" + +/* +Scenario 2 (7 points): API - Query String Builder +Build a function that constructs URL query strings from objects. + +Requirements: +- Handle nested objects (one level deep) +- Encode special characters +- Ignore null/undefined values +- Handle arrays +*/ + +/** + * Function that constructs URL query strings from objects. + * @param {Object} params any object with string, number, array and object data. Null and undefined are not allowed. Only one level deep search. + * @returns {String} URL query strings. + */ +function buildQueryString(params) { + let paramsArray = [] + for (const [key, value] of Object.entries(params)) { + // null + if (value === null) { + continue + } + // undefined + if (value === undefined) { + continue + } + // String: name: "John" => name=John + if (typeof value === "string") { + paramsArray.push(`${key}=${value.replace(/\s+/g, "%20")}`) + } + // Number: page: 1 => page=1 + if (typeof value === "number") { + paramsArray.push(`${key}=${value}`) + } + // Array: tags: ["javascript", "coding"] => tags=javascript&tags=coding + if (Array.isArray(value)) { + for (const element of value) { + // Infered one level deep so array and object are not allowed + // Check string + if (typeof element === "string") { + paramsArray.push(`${key}=${element.replace(/\s+/g, "%20")}`) + } + // Check number + if (typeof element === "number") { + paramsArray.push(`${key}=${element}`) + } + } + } else if (typeof value === "object") { + // Objtect: { user: { id: 123, name: "Alice" } } => user[id]=123&user[name]=Alice + for (const [id, data] of Object.entries(value)) { + // Infered one level deep so array and object are not allowed + // Check string + if (typeof data === "string") { + paramsArray.push(`${key}[${id}]=${data.replace(/\s+/g, "%20")}`) + } + // Check number + if (typeof data === "number") { + paramsArray.push(`${key}[${id}]=${data}`) + } + } + } + } + return paramsArray.join("&") +} + +// Test cases - DO NOT MODIFY +console.log("\n=== Scenario 2: Query String Builder ===") +console.log(buildQueryString({ name: "John", age: 30 })) +// Expected: "name=John&age=30" + +console.log(buildQueryString({ search: "hello world", page: 1 })) +// Expected: "search=hello%20world&page=1" + +console.log(buildQueryString({ tags: ["javascript", "coding"] })) +// Expected: "tags=javascript&tags=coding" + +console.log(buildQueryString({ user: { id: 123, name: "Alice" } })) +// Expected: "user[id]=123&user[name]=Alice" + +console.log(buildQueryString({ a: "test", b: null, c: undefined, d: "value" })) +// Expected: "a=test&d=value" + +/* +Scenario 3 (6 points): Performance - Efficient String Search +Find all occurrences of multiple search terms in a large text. +Return positions and context (10 chars before and after). +*/ + +/** + * Function that find all occurrences of multiple search terms in a large text. + * @param {String} text A large text. + * @param {Array} terms String array search terms. + * @returns Return positions and context (10 chars before and after). + */ +function findAllOccurrences(text, terms) { + // Array of objets + let result = [] + let regularExp + // let addContext + let ocurrence + for (const term of terms) { + regularExp = new RegExp(term, "g") + ocurrence = regularExp.exec(text) + while (ocurrence !== null) { + // get context 10 characters before and after + const context = theContext(ocurrence.index, term, text) + // add result object + result.push({ term: term, position: ocurrence.index, context: context }) + ocurrence = regularExp.exec(text) + } + } + return result.sort(({ position: positionA }, { position: positionB }) => positionA - positionB) +} + +/** + * Function that extract content 10 characters before and after term. + * @param {Number} index term position founded. + * @param {String} term search term. + * @param {String} text any text sentence. + * @returns term plus 10 chars before and 10 chars after. + */ +function theContext(index, term, text) { + let before + let after + let addContext + // check index = 0 there is not need of use before + if (index === 0) { + addContext = text.slice(0, term.length + 10) + if (text.charAt(term.length + 10) !== " ") { + addContext += "..." + } + } else if (index > 0) { + // Before context + if (index - 10 > 0) { + before = text.slice(index - 10, index) + if (before[0] !== " ") { + before = "..." + before + } + } else { + before = text.slice(0, index) + } + /* After context */ + if (index + term.length + 10 > text.length) { + after = text.slice(index + term.length, text.length) + } else { + after = text.slice(index + term.length, index + term.length + 10) + if (after[after.length - 1] !== " ") { + after += "..." + } + } + addContext = before + term + after + } + return addContext +} +// Test case - DO NOT MODIFY +console.log("\n=== Scenario 3: Efficient String Search ===") +const document = "JavaScript is a programming language. JavaScript is widely used." +const searchTerms = ["JavaScript", "language"] + +console.log(findAllOccurrences(document, searchTerms)) +// Expected: [ +// { term: "JavaScript", position: 0, context: "JavaScript is a pr..." }, +// { term: "language", position: 30, context: "...mming language. JavaScr..." }, +// { term: "JavaScript", position: 39, context: "...uage. JavaScript is wide..." } +// ] + +// ======================================== +// END OF EXAM +// ======================================== + +/* +SUBMISSION CHECKLIST: +☐ All theory questions answered +☐ All coding challenges implemented +☐ All scenarios completed +☐ Code tested and runs without errors +☐ Comments explain your logic +☐ Start and end times filled in at top +☐ Ready to submit! + +Save this file as: string-exam-yoandy.js +Submit when complete. +*/ diff --git a/fullstack/javascript/1-variables-strings/greeting-bot/script.js b/fullstack/javascript/1-variables-strings/greeting-bot/script.js new file mode 100644 index 0000000..9b52a48 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/greeting-bot/script.js @@ -0,0 +1,33 @@ +console.log("Hi there!") +console.log("I am excited to talk to you.") +let bot +bot = "teacherBot" + +let botLocation = "the universe" + +console.log("Allow me to introduce myself.") + +const botIntroduction = "My name is " + bot + "." +console.log(botIntroduction) + +const botLocationSentence = "I live in " + botLocation + "." +console.log(botLocationSentence) + +bot = "professorBot" + +const nicknameIntroduction = "My nickname is " + bot + "." +console.log(nicknameIntroduction) + +bot = "awesomeTeacherBot" + +const newNicknameGreeting = + "I love my nickname but I wish people would call me " + bot + "." +console.log(newNicknameGreeting) + +const favoriteSubject = "Computer Science" + +const favoriteSubjectSentence = + "My favorite subject is " + favoriteSubject + "." +console.log(favoriteSubjectSentence) + +console.log("Well, it was nice to talk to you. Have a nice day!") diff --git a/fullstack/javascript/1-variables-strings/prompt/index.html b/fullstack/javascript/1-variables-strings/prompt/index.html new file mode 100644 index 0000000..dbc4970 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/prompt/index.html @@ -0,0 +1,16 @@ + + + + + + + Prompt Practice + + + + +

+ + + + \ No newline at end of file diff --git a/fullstack/javascript/1-variables-strings/prompt/script.js b/fullstack/javascript/1-variables-strings/prompt/script.js new file mode 100644 index 0000000..227d6af --- /dev/null +++ b/fullstack/javascript/1-variables-strings/prompt/script.js @@ -0,0 +1,6 @@ +const btn = document.getElementById("prompt-btn") +const output = document.getElementById("output") +btn.addEventListener("click", () => { + const userName = prompt("What is your name?", "Guest") + output.textContent = "Hello, " + userName + "!" +}) diff --git a/fullstack/javascript/1-variables-strings/sentence-maker/README.md b/fullstack/javascript/1-variables-strings/sentence-maker/README.md new file mode 100644 index 0000000..54f54e9 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/sentence-maker/README.md @@ -0,0 +1,30 @@ +# Build a Sentence Maker + +In this lab, you will create two different stories using a sentence template. You will use variables to store different parts of the story and then output the stories to the console. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +**User Stories**: + +1. [x] You should declare the following variables using **let**: + - adjective + - noun + - verb + - place + - adjective2 + - noun2 +2. [x] You should assign the above variables some string values of your choice. + +3. [x] You should declare a firstStory variable. + +4. [x] You should use the following story template to create the first story and assign it to the firstStory variable: "Once upon a time, there was a(n) [adjective] [noun] who loved to eat [noun2]. The [noun] lived in a [place] and had [adjective2] nostrils that blew fire when it was [verb]."; + +5. [x] You should output your first story to the console using the message "First story: [firstStory]". + +6. [x] You should assign new values to your adjective, noun, verb, place, adjective2, and noun2 variables. + +7. [x] You should declare a secondStory variable. + +8. [x] Create another story using the same template and assign it to the secondStory variable. + +9. [x] You should output your second story to the console using the message "Second story: [secondStory]". diff --git a/fullstack/javascript/1-variables-strings/sentence-maker/script.js b/fullstack/javascript/1-variables-strings/sentence-maker/script.js new file mode 100644 index 0000000..4345b7b --- /dev/null +++ b/fullstack/javascript/1-variables-strings/sentence-maker/script.js @@ -0,0 +1,47 @@ +let adjective = "beautiful" +let noun = "princess" +let verb = "sleeping" +let place = "Palace" +let adjective2 = "huge" +let noun2 = "mango" +let firstStory = + "Once upon a time, there was a(n) " + + adjective + + " " + + noun + + " who loved to eat " + + noun2 + + ". The " + + noun + + " lived in a " + + place + + " and had " + + adjective2 + + " nostrils that blew fire when it was " + + verb + + "." +console.log("First story: " + firstStory) + +adjective = "majestic" +noun = "king" +verb = "running" +place = "Forest" +adjective2 = "big" +noun2 = "apples" +let secondStory = + "Once upon a time, there was a(n) " + + adjective + + " " + + noun + + " who loved to eat " + + noun2 + + ". The " + + noun + + " lived in a " + + place + + " and had " + + adjective2 + + " nostrils that blew fire when it was " + + verb + + "." +console.log("Second story: " + secondStory) diff --git a/fullstack/javascript/1-variables-strings/string-formatter/script.js b/fullstack/javascript/1-variables-strings/string-formatter/script.js new file mode 100644 index 0000000..3afd641 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/string-formatter/script.js @@ -0,0 +1,24 @@ +const userInput = " Hello World! " +console.log(`Original input:`) +console.log(`${userInput}`) +const cleanedInput = userInput.trim() +console.log(`Result of trimming whitespace from both ends:`) +console.log(cleanedInput) +const trimmedStart = userInput.trimStart() +console.log(`After using the trimStart() method, leading spaces removed:`) +console.log(trimmedStart) +const trimmedEnd = userInput.trimEnd() +console.log(`After using the trimEnd() method, trailing spaces removed:`) +console.log(trimmedEnd) +const upperCaseInput = cleanedInput.toUpperCase() +console.log("Result of using the toUpperCase() method:") +console.log(upperCaseInput) +const lowerCaseInput = cleanedInput.toLowerCase() +console.log("Result of using the toLowerCase() method:") +console.log(lowerCaseInput) +const lowercaseWord = "camelcase" +let camelCasedVersion = "" +console.log("Camel cased version:") +console.log(camelCasedVersion) +camelCasedVersion = `${lowercaseWord.slice(0, 5)}${lowercaseWord[5].toUpperCase()}${lowercaseWord.slice(-3)}` +console.log("Camel cased version: " + camelCasedVersion) diff --git a/fullstack/javascript/1-variables-strings/string-inspector/script.js b/fullstack/javascript/1-variables-strings/string-inspector/script.js new file mode 100644 index 0000000..4b002ac --- /dev/null +++ b/fullstack/javascript/1-variables-strings/string-inspector/script.js @@ -0,0 +1,25 @@ +const fccSentence = "freeCodeCamp is a great place to learn web development." +console.log("Here are some examples of the includes() method:") +const hasFreeCodeCamp = fccSentence.includes("freeCodeCamp") ? true : false +console.log( + `fccSentence.includes("freeCodeCamp") returns ${hasFreeCodeCamp} because the word "freeCodeCamp" is in the sentence.` +) +const hasJavaScript = fccSentence.includes("JavaScript") ? true : false +console.log( + `fccSentence.includes("JavaScript") returns ${hasJavaScript} because the word "JavaScript" is not in the sentence.` +) +const hasLowercaseFCC = fccSentence.includes("freecodecamp") ? true : false +console.log( + `fccSentence.includes("freecodecamp") returns ${hasLowercaseFCC} because includes is case-sensitive.` +) +const message = "Welcome to freeCodeCamp!" +console.log(`Here are some examples of the slice() method:`) +const platform = message.slice(11, 23) +console.log(`The word "${platform}" was sliced from the message.`) +const greetingWord = message.slice(0, 7) +console.log(`The first word is "${greetingWord}".`) +const endPunctuation = message.slice(-1) +console.log(`The ending punctuation mark is a "${endPunctuation}"`) +console.log( + `Workshop complete! You now know how to use includes() and slice().` +) diff --git a/fullstack/javascript/1-variables-strings/string-transformer/script.js b/fullstack/javascript/1-variables-strings/string-transformer/script.js new file mode 100644 index 0000000..b66a7cb --- /dev/null +++ b/fullstack/javascript/1-variables-strings/string-transformer/script.js @@ -0,0 +1,18 @@ +const originalString = "I love cats." +console.log(`Original string: ${originalString}`) +const replacedString = originalString.replace("cats", "dogs") +console.log(`After using the replace() method:`) +console.log(replacedString) +const exampleSentence = `I love cats and cats are so much fun!` +console.log(`Original sentence:`) +console.log(exampleSentence) +const dogsOnlySentence = exampleSentence.replaceAll("cats", "dogs") +console.log("Replacing all occurrences of cats with dogs:") +console.log(dogsOnlySentence) +const learningSentence = `I love learning!` +console.log("Original learning sentence:") +console.log(learningSentence) +const repeatedLove = `love `.repeat(3).trimEnd() +console.log(repeatedLove) +const newSentence = `I ${repeatedLove} learning.` +console.log(newSentence) diff --git a/fullstack/javascript/1-variables-strings/teacher-chatbot/script.js b/fullstack/javascript/1-variables-strings/teacher-chatbot/script.js new file mode 100644 index 0000000..b5d2517 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/teacher-chatbot/script.js @@ -0,0 +1,37 @@ +console.log("Hi there!") +const botName = "teacherBot" +const greeting = `My name is ${botName}.` +console.log(greeting) +const subject = "JavaScript" +const topic = "strings" +const sentence = `Today, you will learn about ${topic} in ${subject}.` +console.log(sentence) +const strLengthIntro = `Here is an example of using the length property on the word ${subject}.` +console.log(strLengthIntro) +console.log(subject.length) +console.log( + `Here is an example of using the length property on the word ${topic}.` +) +console.log(topic.length) +console.log( + `Here is an example of accessing the first letter in the word ${subject}.` +) +console.log(subject[0]) +console.log( + `Here is an example of accessing the second letter in the word ${subject}.` +) +console.log(subject[1]) +console.log( + `Here is an example of accessing the last letter in the word ${subject}.` +) +console.log(subject[subject.length - 1]) +const lastCharacter = subject[subject.length - 1] +console.log(lastCharacter) +const learningIsFunSentence = "Learning is fun." +console.log( + "Here are examples of finding the positions of substrings in the sentence." +) +console.log(learningIsFunSentence.indexOf("Learning")) +console.log(learningIsFunSentence.indexOf("fun")) +console.log(learningIsFunSentence.indexOf("learning")) +console.log("I hope you enjoyed learning today.") diff --git a/fullstack/javascript/1-variables-strings/trivia-bot/README.md b/fullstack/javascript/1-variables-strings/trivia-bot/README.md new file mode 100644 index 0000000..fae9f26 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/trivia-bot/README.md @@ -0,0 +1,17 @@ +# Build a JavaScript Trivia Bot + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +**User Stories**: + +1. [x] You should log "Hello! I'm your coding fun fact guide!" to the console as a greeting message to the user. +2. [x] You should create three variables: botName, botLocation, and favoriteLanguage, that store the bot's name, where it's from, and its favorite coding language, respectively. +3. [x] You should log "My name is (botName) and I live on (botLocation)." to the console. +4. [x] You should log "My favorite programming language is (favoriteLanguage)." to the console. +5. [x] You should use let to create a codingFact variable and assign it a string that is a fun fact about your bot's favorite coding language and include the use of the favoriteLanguage variable. +6. [x] You should log the codingFact to the console. +7. [x] You should reassign the codingFact variable to a new fact about the bot's favorite language using the favoriteLanguage variable again. +8. [x] You should log the codingFact to the console again. +9. [x] You should reassign the codingFact variable again to another new fact about the bot's favorite language using the favoriteLanguage variable. +10. [x] You should log the codingFact to the console a third time. +11. [x] You should log "It was fun sharing these facts with you. Goodbye! - (botName) from (botLocation)." to the console as a farewell statement from the bot. diff --git a/fullstack/javascript/1-variables-strings/trivia-bot/script.js b/fullstack/javascript/1-variables-strings/trivia-bot/script.js new file mode 100644 index 0000000..568da00 --- /dev/null +++ b/fullstack/javascript/1-variables-strings/trivia-bot/script.js @@ -0,0 +1,34 @@ +console.log("Hello! I'm your coding fun fact guide!") + +let botName = "mtbot" + +let botLocation = "Earth" + +let favoriteLanguage = "JavaScript" + +console.log("My name is " + botName + " and I live on " + botLocation + ".") + +console.log("My favorite programming language is " + favoriteLanguage + ".") + +let codingFact = favoriteLanguage + " it's not related with Java." + +console.log(codingFact) + +codingFact = + "On a website: HTML it's structure, CSS it's styles and " + + favoriteLanguage + + " it's interactivity." + +console.log(codingFact) + +codingFact = "I'm learning " + favoriteLanguage + " from freeCodeCamp." + +console.log(codingFact) + +console.log( + "It was fun sharing these facts with you. Goodbye! - " + + botName + + " from " + + botLocation + + "." +) diff --git a/fullstack/javascript/3-functions/boolean-check-lab.js b/fullstack/javascript/3-functions/boolean-check-lab.js new file mode 100644 index 0000000..87d133f --- /dev/null +++ b/fullstack/javascript/3-functions/boolean-check-lab.js @@ -0,0 +1,12 @@ +/** + * Function that receives one argument. If the argument received is a boolean primitive, the function return true. If the argument is any other value, the function return false. + * @param {Boolean} argument any boolean. + * @returns {Boolean} If the argument received is a boolean primitive, the function return true. If the argument is any other value, the function return false. + */ +const booWho = (argument) => { + if (argument === true || argument === false) { + return true + } else { + return false + } +} diff --git a/fullstack/javascript/3-functions/boolean-check-lab.md b/fullstack/javascript/3-functions/boolean-check-lab.md new file mode 100644 index 0000000..9000b15 --- /dev/null +++ b/fullstack/javascript/3-functions/boolean-check-lab.md @@ -0,0 +1,13 @@ +# Build a Boolean Check Function + +In this lab, you will build a function that checks if a value is classified as a boolean primitive. + +**Boolean primitives are true and false**. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function called booWho that receives one argument. +2. [x] If the argument received is a boolean primitive, the function should return true. +3. [x] If the argument is any other value, the function should return false. diff --git a/fullstack/javascript/3-functions/card-counting-lab.js b/fullstack/javascript/3-functions/card-counting-lab.js new file mode 100644 index 0000000..9688e08 --- /dev/null +++ b/fullstack/javascript/3-functions/card-counting-lab.js @@ -0,0 +1,39 @@ +let count = 0 + +/** + * Casino game **Blackjack**. This is called Card Counting. Cards 2, 3, 4, 5, or 6 increased by 1. Cards 7, 8 or 9 remain unchanged. Cards 10, "J", "Q", "K" or "A" decreased by 1. Return a string with current count and the string Bet if the count is positive. Return a string with current count and the string Hold if the count is less than or equal to 0. + * @param {String} params card parameter which can either be a number or string. + * @returns {String} Return a string with current count and the string Bet if the count is positive. Return a string with current count and the string Hold if the count is less than or equal to 0. + */ +const cardCounter = (params) => { + let decision + + switch (params) { + case 2: + case 3: + case 4: + case 5: + case 6: + count++ + break + case 7: + case 8: + case 9: + count + break + case 10: + case "J": + case "Q": + case "K": + case "A": + count-- + break + default: + return `Error: Incorrect input, card allowed 2, 3, 4, 5, 6, 7, 8, 9, 10, "J", "Q", "K", "A"` + break + } + count > 0 ? (decision = "Bet") : (decision = "Hold") + return `${count} ${decision}` +} + +console.log(">>>BlackJack Casino", cardCounter(5)) diff --git a/fullstack/javascript/3-functions/card-counting-lab.md b/fullstack/javascript/3-functions/card-counting-lab.md new file mode 100644 index 0000000..b1ecf7a --- /dev/null +++ b/fullstack/javascript/3-functions/card-counting-lab.md @@ -0,0 +1,22 @@ +# Build a Card Counting Assistant + +In the casino game **Blackjack**, a player can determine whether they have an advantage on the next hand over the house by keeping track of the relative number of high and low cards remaining in the deck. This is called Card Counting. + +Having more high cards remaining in the deck favors the player. When the count is positive, the player should bet high. When the count is zero or negative, the player should bet low. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should use let to declare a global variable named count and set it to 0. +2. [x] You should have a function called cardCounter. +3. The cardCounter function should receive a card parameter which can either be a number or string. + - For values between 2 to 10, the card parameter will be a number. + - For all other values, the card parameter will be a string. +4. [x] The cardCounter function should modify the global count variable based on certain criteria. +5. [x] The global count variable should be increased by 1 for the cards 2, 3, 4, 5, or 6 +6. [x] The global count variable should remain unchanged for the cards 7, 8, 9. +7. [x] The global count variable should be decreased by 1 for the cards 10, "J", "Q", "K", "A" +8. [x] The cardCounter function should return a string with current count and the string Bet if the count is positive. +9. [x] The cardCounter function should return a string with current count and the string Hold if the count is less than or equal to 0. +10. [x] In the function output, the current count and the player's decision (Bet or Hold) should be separated by a space. For example, -3 Hold. diff --git a/fullstack/javascript/3-functions/celsius-lab.js b/fullstack/javascript/3-functions/celsius-lab.js new file mode 100644 index 0000000..3b139f1 --- /dev/null +++ b/fullstack/javascript/3-functions/celsius-lab.js @@ -0,0 +1,13 @@ +/** + * Celsius to Fahrenheit Converter + * @param {Number} celsius any temperature in Celsius. + * @returns {Number} return the temperature in Fahrenheit. + */ +const convertCtoF = (celsius) => { + return celsius * (9 / 5) + 32 +} + +console.log(convertCtoF(0)) +console.log(convertCtoF(20)) +console.log(convertCtoF(37)) +console.log(convertCtoF(100)) diff --git a/fullstack/javascript/3-functions/celsius-lab.md b/fullstack/javascript/3-functions/celsius-lab.md new file mode 100644 index 0000000..dca1f52 --- /dev/null +++ b/fullstack/javascript/3-functions/celsius-lab.md @@ -0,0 +1,13 @@ +# Build a Celsius to Fahrenheit Converter + +In this lab, you will write a function that converts the temperature from Celsius to Fahrenheit. The formula to convert from Celsius to Fahrenheit is: + +**fahrenheit = celsius \* (9/5) + 32** + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named convertCtoF. +2. [x] convertCtoF should take a single numeric argument, which is the temperature in Celsius. +3. [x] convertCtoF should return a number. diff --git a/fullstack/javascript/3-functions/email-masker-lab.js b/fullstack/javascript/3-functions/email-masker-lab.js new file mode 100644 index 0000000..d647f0f --- /dev/null +++ b/fullstack/javascript/3-functions/email-masker-lab.js @@ -0,0 +1,18 @@ +/** + * Function that takes email as an argument. Mask the email and append the domain name to it. + * @param {String} email any email. + * @returns {String} masked email apple.pie@example.com = a*******e@example.com. + */ +const maskEmail = (email) => { + const [name, domain] = email.split("@") + const masked = `${name[0]}${"*".repeat(name.length - 2)}${name[name.length - 1]}` + return `${masked}@${domain}` +} + +let email = "apple.pie@example.com" +console.log(maskEmail(email)) + +console.log(maskEmail("apple.pie@example.com")) +console.log(maskEmail("freecodecamp@example.com")) +console.log(maskEmail("info@test.dev")) +console.log(maskEmail("user@domain.org")) diff --git a/fullstack/javascript/3-functions/email-masker-lab.md b/fullstack/javascript/3-functions/email-masker-lab.md new file mode 100644 index 0000000..7be464e --- /dev/null +++ b/fullstack/javascript/3-functions/email-masker-lab.md @@ -0,0 +1,19 @@ +# Build an Email Masker + +In this lab, you will mask the username part of an email address with asterisks. Masking is a term used to hide or replace sensitive information with asterisks or other characters. + +For example, if the email address was myEmail@email.com, then the masked email address will be m**\***l@email.com. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Create a function named maskEmail that takes email as an argument. +2. [x] Inside the function, you should mask the email and append the domain name to it. Remember that you can use methods like slice, repeat, indexOf or even replace to help you. +3. [x] Outside the function, declare a variable named email to store the email address you want to mask. +4. [x] Call the maskEmail function with the email variable and output the result to the console. + +- maskEmail("apple.pie@example.com") should return "a**\*\*\***e@example.com". +- maskEmail("freecodecamp@example.com") should return f\***\*\*\*\*\***p@example.com". +- maskEmail("info@test.dev") should return "i\*\*o@test.dev". +- maskEmail("user@domain.org") should return "u\*\*r@domain.org". diff --git a/fullstack/javascript/3-functions/ending-tool-lab.js b/fullstack/javascript/3-functions/ending-tool-lab.js new file mode 100644 index 0000000..f3e7542 --- /dev/null +++ b/fullstack/javascript/3-functions/ending-tool-lab.js @@ -0,0 +1,34 @@ +/** + * Function that checks if a string ends with the given target string. + * @param {String} sentence any sentence. + * @param {String} strToCheckAgainst the given target string to sentence check. + * @returns {Boolean} Return true if the first string ends with the second string, and false otherwise. + */ +function confirmEnding(sentence, strToCheckAgainst) { + let decision + // 1. check length: sentence should be bigger than strToCheckAgainst + if (sentence.length < strToCheckAgainst.length) { + return false + } + // 2. Extract string ends from sentence + let strToCheck = sentence.slice(-strToCheckAgainst.length) + strToCheck === strToCheckAgainst ? (decision = true) : (decision = false) + return decision +} +console.log(confirmEnding("Bastian", "n")) //true +console.log(confirmEnding("Congratulation", "on")) //true +console.log(confirmEnding("Connor", "n")) //false +console.log( + confirmEnding("Walking on water and developing software from a specification are easy if both are frozen", "specification"), +) //false +console.log(confirmEnding("He has to give me a new name", "name")) //true +console.log(confirmEnding("Open sesame", "same")) //true +console.log(confirmEnding("Open sesame", "sage")) //false +console.log(confirmEnding("Open sesame", "game")) //false +console.log( + confirmEnding( + "If you want to save our world, you must hurry. We don't know how much longer we can withstand the nothing", + "mountain", + ), +) //false +console.log(confirmEnding("Abstraction", "action")) //true diff --git a/fullstack/javascript/3-functions/ending-tool-lab.md b/fullstack/javascript/3-functions/ending-tool-lab.md new file mode 100644 index 0000000..3f4a1a5 --- /dev/null +++ b/fullstack/javascript/3-functions/ending-tool-lab.md @@ -0,0 +1,11 @@ +# Build a Confirm the Ending Tool + +In this lab, you will implement a function that checks if a string ends with the given target string. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named confirmEnding that takes two parameters: the string to check and the string to check against. +2. [x] The function should return true if the first string ends with the second string, and false otherwise. +3. [x] You should not use the .endsWith() method; instead, use one of the JavaScript substring methods to achieve this. diff --git a/fullstack/javascript/3-functions/leap-year-calculator.js b/fullstack/javascript/3-functions/leap-year-calculator.js new file mode 100644 index 0000000..1b08834 --- /dev/null +++ b/fullstack/javascript/3-functions/leap-year-calculator.js @@ -0,0 +1,20 @@ +/** + * Leap Year Calculator. A leap year is a year that is divisible by 4, except for years that are divisible by 100 and not divisible by 400. For example, 2000 is a leap year, but 1900 is not. Also, a leap year has an extra day in February, which is the 29th day of the month. + * @param {Number} year any year. + * @returns {Boolean} + */ +const isLeapYear = (year) => { + let decision + if (year % 100 === 0 && year % 400 !== 0) { + decision = "is not a leap year." + } else if (year % 4 === 0) { + decision = "is a leap year." + } else { + decision = "is not a leap year." + } + return `${year} ${decision}` +} + +const year = 2024 +const result = isLeapYear(year) +console.log(result) diff --git a/fullstack/javascript/3-functions/leap-year-calculator.md b/fullstack/javascript/3-functions/leap-year-calculator.md new file mode 100644 index 0000000..33324e2 --- /dev/null +++ b/fullstack/javascript/3-functions/leap-year-calculator.md @@ -0,0 +1,18 @@ +# Build a Leap Year Calculator + +A leap year is a year that is divisible by 4, except for years that are divisible by 100 and not divisible by 400. For example, 2000 is a leap year, but 1900 is not. Also, a leap year has an extra day in February, which is the 29th day of the month. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Define a function called isLeapYear that takes a number as an argument. +2. [x] Outside the function, declare a variable year that stores the value of the year you want to check. +3. [x] Inside the function, use an if/ else statement or a ternary operator to check if the year is a leap year. +4. [x] To check if the year is a leap year, fulfill the following conditions: + - If the year is divisible by 4, then it is a leap year. + - Unless the year is also divisible by 100, then it is not a leap year. + - Unless the year is also divisible by 400, then it is a leap year. +5. [x] If the year is a leap year, return [year] is a leap year.. Otherwise, return [year] is not a leap year.. You will replace [year] with the parameter defined in the isLeapYear function. +6. [x] You should call the isLeapYear function with year as the argument and assign the result to a variable named result. +7. [x] You should output the result variable to the console using console.log(). diff --git a/fullstack/javascript/3-functions/study/ch3-study.js b/fullstack/javascript/3-functions/study/ch3-study.js new file mode 100644 index 0000000..4be58e1 --- /dev/null +++ b/fullstack/javascript/3-functions/study/ch3-study.js @@ -0,0 +1,1025 @@ +// ============================================= +// main.js - Ejemplo Completo Vanilla JS +// ============================================= + +const usuario = { + nombre: "Yoandy", + edad: 30, + skills: ["React", "TypeScript", "Node.js", "Tailwind"], + saludar() { + console.log(`Hola, soy ${this.nombre}`) + this.mostrarEdad() // Llamando a otro método del mismo objeto + this.mostrarSkills() + }, + mostrarEdad() { + console.log(`Tengo ${this.edad} años`) + }, + mostrarSkills() { + console.log("Mis skills:") + this.skills.forEach((skill) => console.log(` - ${skill}`)) + }, + + // Ejemplo con closure + acumulador dentro del objeto + crearContador() { + let contador = 0 // Variable privada gracias al closure + + return { + incrementar: function () { + contador++ + console.log(`Contador: ${contador}`) + return contador + }, + decrementar: function () { + contador-- + console.log(`Contador: ${contador}`) + return contador + }, + obtenerValor: function () { + return contador + }, + reset: function () { + contador = 0 + console.log("Contador reiniciado") + }, + } + }, +} + +// ======================== +// Uso del objeto +// ======================== +console.log("%c=== Ejecución del objeto ===", "color: cyan; font-weight: bold") +usuario.saludar() + +// Crear un contador usando closure +const miContador = usuario.crearContador() + +console.log("%c=== Probando Closure + Acumulador ===", "color: lime; font-weight: bold") +miContador.incrementar() +miContador.incrementar() +miContador.incrementar() +miContador.decrementar() +console.log("Valor actual:", miContador.obtenerValor()) +miContador.reset() + +// ============================================= +// Ejemplos Avanzados de Closures y Acumuladores +// ============================================= + +/** + * 1. Closure simple con acumulador (contador privado) + */ +function crearContadorInicial(valorInicial = 0) { + let contador = valorInicial + + return { + sumar: (cantidad = 1) => { + contador += cantidad + return contador + }, + restar: (cantidad = 1) => { + contador -= cantidad + return contador + }, + valor: () => contador, + reset: () => { + contador = valorInicial + return contador + }, + } +} + +const contador1 = crearContadorInicial(10) +console.log("Contador1:", contador1.valor()) // 10 +contador1.sumar(5) // 15 +contador1.sumar() // 16 + +/** + * 2. Acumulador de total (como un reduce pero persistente) + */ +function crearAcumulador() { + let total = 0 + let historial = [] + + return { + agregar: (monto) => { + total += monto + historial.push(monto) + console.log(`Agregado: \[ {monto} | Total: \]{total}`) + return total + }, + obtenerTotal: () => total, + obtenerHistorial: () => [...historial], // copia para no romper encapsulación + promedio: () => (historial.length ? total / historial.length : 0), + reset: () => { + total = 0 + historial = [] + }, + } +} + +const caja = crearAcumulador() +caja.agregar(100) +caja.agregar(250) +caja.agregar(75) +console.log("Total final:", caja.obtenerTotal()) +console.log("Promedio:", caja.promedio()) + +// ============================================= +// Diferencia entre reduce() y Closure +// ============================================= + +const numeros = [10, 20, 30, 40] + +// Con reduce (una sola operación) +const sumaTotal = numeros.reduce((acumulador, numero) => acumulador + numero, 0) +console.log("Suma con reduce:", sumaTotal) + +// Con closure (estado persistente a lo largo del tiempo) +function crearAcumuladorPersistente() { + let suma = 0 + + return (nuevoNumero) => { + suma += nuevoNumero + console.log(`Acumulando... +${nuevoNumero} = ${suma}`) + return suma + } +} + +const acumular = crearAcumuladorPersistente() +acumular(10) +acumular(20) +acumular(30) // mantiene el estado entre llamadas + +// ======================================================== +// patrones-funciones-avanzadas.js +// Archivo educativo completo - Vanilla JS +// Autor: Senior Front-end Fullstack +// ======================================================== + +console.log("%c=== PATRONES DE DISEÑO, CLOSURES, IIFE Y MÓDULOS ===", "color: #00ffcc; font-size: 16px; font-weight: bold") + +/* ============================================================= + 1. IIFE - Immediately Invoked Function Expression + ============================================================= */ +const IIFE = (function () { + // Variables privadas + let contadorPrivado = 0 + const nombreApp = "MiAppVanilla" + + function incrementar() { + contadorPrivado++ + console.log(`[IIFE] Contador privado: ${contadorPrivado}`) + } + + // API pública + return { + saludar: () => console.log(`Hola desde ${nombreApp}`), + incrementarContador: incrementar, + obtenerContador: () => contadorPrivado, + } +})() + +// Uso del IIFE +IIFE.saludar() +IIFE.incrementarContador() +IIFE.incrementarContador() + +/* ============================================================= + 2. CLOSURES + Acumuladores + Estado Persistente + ============================================================= */ + +/** + * Contador con delay (setTimeout) + */ +function crearContadorConDelay() { + let count = 0 + let timer = null + + function incrementarConDelay(ms = 1000) { + if (timer) clearTimeout(timer) + + timer = setTimeout(() => { + count++ + console.log(`%c[Delay] Contador aumentó a: ${count}`, "color: orange") + }, ms) + } + + return { + incrementar: incrementarConDelay, + valor: () => count, + reset: () => { + count = 0 + }, + } +} + +const contadorDelay = crearContadorConDelay() +contadorDelay.incrementar(800) +contadorDelay.incrementar(1500) // Sobrescribe el timer anterior + +/* ============================================================= + 3. MINI ESTADO GLOBAL SEGURO (Pattern muy usado en Vanilla JS) + ============================================================= */ + +const EstadoGlobal = (function () { + // Estado privado (closure) + let state = { + usuario: null, + tema: "light", + notificaciones: 0, + carrito: [], + } + + const listeners = new Set() // Para patrón Observer básico + + function notify() { + listeners.forEach((callback) => callback(state)) + } + + return { + getState: () => ({ ...state }), // Retorna copia + + setUsuario: (usuario) => { + state.usuario = usuario + notify() + console.log("%c[Estado] Usuario actualizado", "color: lime", usuario) + }, + + cambiarTema: (nuevoTema) => { + state.tema = nuevoTema + document.documentElement.setAttribute("data-theme", nuevoTema) + notify() + }, + + agregarAlCarrito: (producto) => { + state.carrito.push(producto) + state.notificaciones++ + notify() + console.log(`Producto agregado: ${producto.nombre}`) + }, + + subscribe: (callback) => { + listeners.add(callback) + // Retorna función para desuscribirse + return () => listeners.delete(callback) + }, + + reset: () => { + state = { usuario: null, tema: "light", notificaciones: 0, carrito: [] } + notify() + }, + } +})() + +// Ejemplo de uso del estado global seguro +EstadoGlobal.subscribe((nuevoEstado) => { + console.log("%c[Observer] Estado cambió:", "color: violet", nuevoEstado) +}) + +EstadoGlobal.setUsuario({ id: 1, nombre: "Yoandy" }) +EstadoGlobal.cambiarTema("dark") +EstadoGlobal.agregarAlCarrito({ id: 101, nombre: "Curso JS Avanzado", precio: 89 }) + +/* ============================================================= + 4. PATRONES DE DISEÑO MÁS USADOS EN VANILLA JS + ============================================================= */ + +// 4.1 Module Pattern (Moderno con IIFE) +const ModuloUsuario = (function () { + let usuarios = [] + + function agregar(usuario) { + usuarios.push(usuario) + console.log(`Usuario agregado: ${usuario.nombre}`) + } + + function listar() { + console.table(usuarios) + return usuarios + } + + function buscarPorId(id) { + return usuarios.find((u) => u.id === id) + } + + // API pública + return { agregar, listar, buscarPorId } +})() + +ModuloUsuario.agregar({ id: 1, nombre: "Yoandy", rol: "Senior" }) + +// 4.2 Factory Pattern +function crearPersona(nombre, edad) { + let _edad = edad // privada + + return { + nombre, + saludar() { + console.log(`Hola, me llamo ${this.nombre} y tengo ${_edad} años`) + }, + cumplirAnios() { + _edad++ + console.log(`${this.nombre} cumplió años! Ahora tiene ${_edad}`) + }, + } +} + +const persona1 = crearPersona("Ana", 28) +persona1.saludar() +persona1.cumplirAnios() + +// 4.3 Singleton Pattern (usando closure) +const SingletonDB = (function () { + let instancia = null + + function crearInstancia() { + return { + conexion: "Activa", + query: (sql) => console.log(`Ejecutando: ${sql}`), + } + } + + return { + getInstance: () => { + if (!instancia) { + instancia = crearInstancia() + console.log("Nueva conexión a BD creada") + } + return instancia + }, + } +})() + +const db1 = SingletonDB.getInstance() +const db2 = SingletonDB.getInstance() +console.log("¿Son la misma instancia?", db1 === db2) + +// 4.4 Revealing Module Pattern +const Calculadora = (function () { + function sumar(a, b) { + return a + b + } + function restar(a, b) { + return a - b + } + function multiplicar(a, b) { + return a * b + } + + // Solo revelamos lo que queremos + return { + suma: sumar, + resta: restar, + multiplicacion: multiplicar, + version: "2.0", + } +})() + +/* ============================================================= + 5. RESUMEN DE CONCEPTOS IMPORTANTES SOBRE FUNCIONES + ============================================================= */ + +console.log("%c=== CONCEPTOS CLAVE SOBRE FUNCIONES ===", "color: #ffcc00; font-weight: bold") + +/* +1. Function Declaration (hoisted) +2. Function Expression +3. Arrow Functions (sin this propio) +4. IIFE +5. Higher-Order Functions (reciben o devuelven funciones) +6. Closures (captura de variables) +7. Currying +8. Partial Application +9. Factory Functions +10. Module Pattern +*/ + +// Ejemplo de Currying +const curriedSuma = (a) => (b) => (c) => a + b + c +console.log("Currying:", curriedSuma(5)(10)(15)) + +// Ejemplo de Higher-Order Function +function conLogger(fn) { + return function (...args) { + console.log(`%c[Llamando función]: ${fn.name}`, "color: cyan") + return fn(...args) + } +} + +const sumaConLog = conLogger((x, y) => x + y) +sumaConLog(7, 8) + +// ======================================================== +// patrones-avanzados-vanilla.js +// Patrones Avanzados, Arquitectura y Buenas Prácticas +// Vanilla JS - Senior Front-end Fullstack +// ======================================================== + +console.log("%c=== PATRONES AVANZADOS EN VANILLA JS ===", "color: #00ff88; font-size: 18px; font-weight: bold") + +/* ============================================================= + 1. EVENT EMITTER PERSONALIZADO (muy útil) + ============================================================= */ +class EventEmitter { + constructor() { + this.events = new Map() + } + + on(eventName, callback) { + if (!this.events.has(eventName)) { + this.events.set(eventName, []) + } + this.events.get(eventName).push(callback) + return () => this.off(eventName, callback) // Retorna unsubscribe + } + + off(eventName, callback) { + if (!this.events.has(eventName)) return + const callbacks = this.events.get(eventName) + this.events.set( + eventName, + callbacks.filter((cb) => cb !== callback), + ) + } + + emit(eventName, ...args) { + if (!this.events.has(eventName)) return + this.events.get(eventName).forEach((callback) => { + try { + callback(...args) + } catch (error) { + console.error(`Error en evento ${eventName}:`, error) + } + }) + } + + once(eventName, callback) { + const wrapper = (...args) => { + callback(...args) + this.off(eventName, wrapper) + } + this.on(eventName, wrapper) + } +} + +// Uso del EventEmitter +const emitter = new EventEmitter() + +const unsubscribe = emitter.on("user:login", (usuario) => { + console.log(`%c[Evento] Usuario inició sesión: ${usuario.nombre}`, "color: gold") +}) + +emitter.emit("user:login", { nombre: "Yoandy", rol: "Senior" }) +unsubscribe() // Desuscribirse + +/* ============================================================= + 2. PUB/SUB PATTERN (Publicador / Suscriptor) + ============================================================= */ +const PubSub = (function () { + const topics = new Map() + let id = 0 + + return { + subscribe: (topic, listener) => { + if (!topics.has(topic)) topics.set(topic, []) + + const subscriberId = ++id + topics.get(topic).push({ id: subscriberId, listener }) + + return () => { + const listeners = topics.get(topic) + topics.set( + topic, + listeners.filter((sub) => sub.id !== subscriberId), + ) + } + }, + + publish: (topic, data) => { + if (!topics.has(topic)) return + topics.get(topic).forEach((subscriber) => { + try { + subscriber.listener(data) + } catch (e) { + console.error(`Error en topic ${topic}:`, e) + } + }) + }, + } +})() + +// Ejemplo Pub/Sub +PubSub.subscribe("cart:updated", (carrito) => { + console.log(`%c[PubSub] Carrito actualizado → ${carrito.length} productos`, "color: hotpink") +}) + +PubSub.publish("cart:updated", [ + { id: 1, name: "Curso JS" }, + { id: 2, name: "Tailwind" }, +]) + +/* ============================================================= + 3. DECORADORES (Decorator Pattern) + ============================================================= */ +function logExecution(targetFunction, functionName = targetFunction.name) { + return function (...args) { + console.time(functionName) + const result = targetFunction(...args) + console.timeEnd(functionName) + console.log(`%c[Decorador] ${functionName} ejecutado con args:`, "color: cyan", args) + return result + } +} + +function debounce(fn, delay = 300) { + let timeout + return function (...args) { + clearTimeout(timeout) + timeout = setTimeout(() => fn(...args), delay) + } +} + +// Uso +const buscar = logExecution((query) => { + console.log(`Buscando: ${query}`) + return `Resultados para ${query}` +}, "buscar") + +buscar("JavaScript Avanzado") + +/* ============================================================= + 4. MEMOIZATION (Optimización de funciones puras) + ============================================================= */ +function memoize(fn) { + const cache = new Map() + + return function (...args) { + const key = JSON.stringify(args) + if (cache.has(key)) { + console.log(`[Memoization] ✅ Cache hit para: ${key}`) + return cache.get(key) + } + + const result = fn(...args) + cache.set(key, result) + console.log(`[Memoization] 📦 Cache miss - Guardado: ${key}`) + return result + } +} + +const fibonacci = memoize(function (n) { + if (n <= 1) return n + return fibonacci(n - 1) + fibonacci(n - 2) +}) + +console.log("Fib(10) =", fibonacci(10)) +console.log("Fib(12) =", fibonacci(12)) // Usará caché para valores anteriores + +/* ============================================================= + 5. EVENT DELEGATION (Gestión avanzada de eventos) + ============================================================= */ +function initEventDelegation() { + const container = document.createElement("div") + container.id = "dynamic-container" + document.body.appendChild(container) + + // Event Delegation - Un solo listener para muchos elementos + container.addEventListener("click", function (e) { + if (e.target.matches(".btn-delete")) { + console.log("Eliminar elemento:", e.target.dataset.id) + e.target.closest(".item").remove() + } + + if (e.target.matches(".btn-edit")) { + console.log("Editar elemento:", e.target.dataset.id) + } + }) + + // Agregar elementos dinámicamente + container.innerHTML = ` +
+

Producto 1

+ + +
+ ` +} + +initEventDelegation() + +/* ============================================================= + 6. ESTRUCTURA DE PROYECTO GRANDE EN VANILLA JS + ============================================================= */ + +/* +Estructura recomendada (Scalable Vanilla JS Architecture): + +/proyecto-vanilla/ +├── index.html +├── /assets/ +│ ├── css/ +│ └── images/ +├── /js/ +│ ├── /core/ ← Base del framework +│ │ ├── eventEmitter.js +│ │ ├── pubsub.js +│ │ ├── store.js +│ │ └── utils.js +│ ├── /modules/ ← Módulos por dominio +│ │ ├── auth/ +│ │ │ ├── auth.module.js +│ │ │ └── auth.ui.js +│ │ ├── cart/ +│ │ ├── products/ +│ │ └── ui/ +│ ├── /components/ ← Web Components o Custom Elements +│ ├── /services/ ← API calls +│ └── main.js ← Entry point +├── /utils/ +└── package.json (opcional con Vite/Parcel) +*/ + +// Ejemplo de main.js (Entry Point) +const App = (function () { + let initialized = false + + function init() { + if (initialized) return + + console.log("%c🚀 Aplicación Vanilla JS inicializada", "color: lime; font-size: 14px") + + // Inicializar módulos + // AuthModule.init(); + // CartModule.init(); + // EventEmitter.global = new EventEmitter(); + + initialized = true + } + + return { init } +})() + +// Auto-inicialización cuando el DOM esté listo +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", App.init) +} else { + App.init() +} + +// ======================================================== +// app-avanzada-vanilla.js +// Mini Aplicación Completa: ToDo + Carrito +// Web Components + Proxy State + Vanilla Router +// Senior Front-end Fullstack +// ======================================================== + +console.log("%c🚀 App Avanzada Vanilla JS - Iniciada", "color:#00ffcc; font-size:18px; font-weight:bold") + +/* ============================================================= + 1. STATE MANAGEMENT AVANZADO CON PROXY + ============================================================= */ +const createStore = (initialState) => { + let state = { ...initialState } + const listeners = new Set() + + const proxy = new Proxy(state, { + set(target, property, value) { + target[property] = value + notify() + return true + }, + }) + + function notify() { + listeners.forEach((listener) => listener(proxy)) + } + + return { + state: proxy, + subscribe: (listener) => { + listeners.add(listener) + listener(proxy) // Llamada inicial + return () => listeners.delete(listener) + }, + setState: (newState) => { + Object.assign(state, newState) + notify() + }, + } +} + +// Store global de la aplicación +const appStore = createStore({ + todos: [], + cart: [], + route: "home", + theme: "light", +}) + +/* ============================================================= + 2. WEB COMPONENTS + CUSTOM ELEMENTS + ============================================================= */ + +// Componente: Todo Item +class TodoItem extends HTMLElement { + constructor() { + super() + this.attachShadow({ mode: "open" }) + } + + connectedCallback() { + this.render() + this.addEventListeners() + } + + set todo(data) { + this._todo = data + if (this.shadowRoot) this.render() + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ + ${this._todo.text} + +
+ ` + } + + addEventListeners() { + const checkbox = this.shadowRoot.querySelector("input") + const deleteBtn = this.shadowRoot.querySelector(".delete") + + checkbox.addEventListener("change", () => { + window.dispatchEvent(new CustomEvent("todo:toggle", { detail: this._todo.id })) + }) + + deleteBtn.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent("todo:delete", { detail: this._todo.id })) + }) + } +} + +customElements.define("todo-item", TodoItem) + +// Componente: Cart Item +class CartItem extends HTMLElement { + connectedCallback() { + this.render() + } + + set item(data) { + this._item = data + this.render() + } + + render() { + this.innerHTML = ` +
+ ${this._item.name} × ${this._item.quantity} + \[ {(this._item.price * this._item.quantity).toFixed(2)} +
+ ` + } +} + +customElements.define("cart-item", CartItem) + +/* ============================================================= + 3. VANILLA ROUTER + ============================================================= */ +class VanillaRouter { + constructor() { + this.routes = new Map() + window.addEventListener("popstate", () => this.handleRoute()) + } + + addRoute(path, callback) { + this.routes.set(path, callback) + } + + navigate(path) { + history.pushState({}, "", path) + this.handleRoute() + } + + handleRoute() { + const path = window.location.pathname || "/" + const handler = this.routes.get(path) || this.routes.get("*") + if (handler) handler() + } +} + +const router = new VanillaRouter() + +router.addRoute("/", () => appStore.setState({ route: "home" })) +router.addRoute("/cart", () => appStore.setState({ route: "cart" })) +router.addRoute("*", () => appStore.setState({ route: "home" })) + +/* ============================================================= + 4. MINI APLICACIÓN - LÓGICA PRINCIPAL + ============================================================= */ + +const AppFour = { + init() { + this.bindEvents() + this.render() + router.handleRoute() + }, + + bindEvents() { + // Eventos globales + window.addEventListener("todo:add", (e) => { + const todos = [ + ...appStore.state.todos, + { + id: Date.now(), + text: e.detail, + completed: false, + }, + ] + appStore.setState({ todos }) + }) + + window.addEventListener("todo:toggle", (e) => { + const todos = appStore.state.todos.map((todo) => (todo.id === e.detail ? { ...todo, completed: !todo.completed } : todo)) + appStore.setState({ todos }) + }) + + window.addEventListener("todo:delete", (e) => { + const todos = appStore.state.todos.filter((todo) => todo.id !== e.detail) + appStore.setState({ todos }) + }) + + // Suscripción al store + appStore.subscribe(() => this.render()) + }, + + addTodo(text) { + window.dispatchEvent(new CustomEvent("todo:add", { detail: text })) + }, + + addToCart(product) { + const cart = [...appStore.state.cart] + const existing = cart.find((item) => item.id === product.id) + + if (existing) { + existing.quantity += 1 + } else { + cart.push({ ...product, quantity: 1 }) + } + + appStore.setState({ cart }) + }, + + render() { + const { todos, cart, route } = appStore.state + + // Renderizar según ruta (SPA simple) + if (route === "cart") { + this.renderCart(cart) + } else { + this.renderHome(todos) + } + }, + + renderHome(todos) { + const container = document.getElementById("app-container") + if (!container) return + + container.innerHTML = ` +

ToDo + Carrito (Vanilla JS Avanzado)

+ + + + +

Tareas (${todos.length})

+
+ +

Productos

+ + + +

Ir al Carrito (${cart.length} productos)

+ ` + + // Renderizar todos con Web Component + const todoList = document.getElementById("todo-list") + todos.forEach((todo) => { + const todoEl = document.createElement("todo-item") + todoEl.todo = todo + todoList.appendChild(todoEl) + }) + + // Evento agregar todo + document.getElementById("add-todo").addEventListener("click", () => { + const input = document.getElementById("todo-input") + if (input.value.trim()) { + this.addTodo(input.value.trim()) + input.value = "" + } + }) + }, + + renderCart(cart) { + const container = document.getElementById("app-container") + const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0) + + container.innerHTML = ` +

🛒 Carrito de Compras

+
+

Total: \]{total.toFixed(2)}

+ + ` + + const cartContainer = document.getElementById("cart-items") + cart.forEach((item) => { + const cartEl = document.createElement("cart-item") + cartEl.item = item + cartContainer.appendChild(cartEl) + }) + }, +} + +/* ============================================================= + 5. PERFORMANCE & BUENAS PRÁCTICAS + ============================================================= */ + +// Lazy Loading de imágenes (ejemplo) +function lazyLoadImages() { + const images = document.querySelectorAll("img[data-src]") + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target + img.src = img.dataset.src + observer.unobserve(img) + } + }) + }) + + images.forEach((img) => observer.observe(img)) +} + +// Debounce para búsqueda +const debouncePerformance = (fn, delay) => { + let timeout + return (...args) => { + clearTimeout(timeout) + timeout = setTimeout(() => fn(...args), delay) + } +} + +/* ============================================================= + 6. TESTING BÁSICO (Jest-like) + ============================================================= */ + +const assert = (condition, message) => { + if (!condition) throw new Error(message || "Test failed") +} + +const test = (name, fn) => { + try { + fn() + console.log(`✅ ${name}`) + } catch (e) { + console.error(`❌ ${name}: ${e.message}`) + } +} + +// Tests +test("Store debería actualizar estado", () => { + const testStore = createStore({ count: 0 }) + testStore.setState({ count: 5 }) + assert(testStore.state.count === 5) +}) + +test("Router debería navegar", () => { + router.navigate("/cart") + assert(appStore.state.route === "cart") +}) + +/* ============================================================= + 7. INICIALIZACIÓN + ============================================================= */ + +document.addEventListener("DOMContentLoaded", () => { + AppFour.init() + lazyLoadImages() + + console.log("%c✅ Aplicación completa cargada correctamente", "color:lime; font-weight:bold") +}) + +// Exponer App globalmente para botones inline +window.App = AppFour +window.router = router diff --git a/fullstack/javascript/3-functions/study/main-study.js b/fullstack/javascript/3-functions/study/main-study.js new file mode 100644 index 0000000..25d2206 --- /dev/null +++ b/fullstack/javascript/3-functions/study/main-study.js @@ -0,0 +1,1028 @@ +// ============================================= +// main.js - Ejemplo Completo Vanilla JS +// ============================================= + +const usuario = { + nombre: "Yoandy", + edad: 30, + skills: ["React", "TypeScript", "Node.js", "Tailwind"], + saludar() { + console.log(`Hola, soy ${this.nombre}`) + this.mostrarEdad() // Llamando a otro método del mismo objeto + this.mostrarSkills() + }, + + mostrarEdad() { + console.log(`Tengo ${this.edad} años`) + }, + + mostrarSkills() { + console.log("Mis skills:") + this.skills.forEach((skill) => console.log(` - ${skill}`)) + }, + + // Ejemplo con closure + acumulador dentro del objeto + crearContador() { + let contador = 0 // Variable privada gracias al closure + + return { + incrementar: function () { + contador++ + console.log(`Contador: ${contador}`) + return contador + }, + decrementar: function () { + contador-- + console.log(`Contador: ${contador}`) + return contador + }, + obtenerValor: function () { + return contador + }, + reset: function () { + contador = 0 + console.log("Contador reiniciado") + }, + } + }, +} + +// ======================== +// Uso del objeto +// ======================== +console.log("%c=== Ejecución del objeto ===", "color: cyan; font-weight: bold") +usuario.saludar() + +// Crear un contador usando closure +const miContador = usuario.crearContador() + +console.log("%c=== Probando Closure + Acumulador ===", "color: lime; font-weight: bold") +miContador.incrementar() +miContador.incrementar() +miContador.incrementar() +miContador.decrementar() +console.log("Valor actual:", miContador.obtenerValor()) +miContador.reset() + +// ============================================= +// Ejemplos Avanzados de Closures y Acumuladores +// ============================================= + +/** + * 1. Closure simple con acumulador (contador privado) + */ +function crearContadorInicial(valorInicial = 0) { + let contador = valorInicial + + return { + sumar: (cantidad = 1) => { + contador += cantidad + return contador + }, + restar: (cantidad = 1) => { + contador -= cantidad + return contador + }, + valor: () => contador, + reset: () => { + contador = valorInicial + return contador + }, + } +} + +const contador1 = crearContadorInicial(10) +console.log("Contador1:", contador1.valor()) // 10 +contador1.sumar(5) // 15 +contador1.sumar() // 16 + +/** + * 2. Acumulador de total (como un reduce pero persistente) + */ +function crearAcumulador() { + let total = 0 + let historial = [] + + return { + agregar: (monto) => { + total += monto + historial.push(monto) + console.log(`Agregado: \[ {monto} | Total: \]{total}`) + return total + }, + obtenerTotal: () => total, + obtenerHistorial: () => [...historial], // copia para no romper encapsulación + promedio: () => (historial.length ? total / historial.length : 0), + reset: () => { + total = 0 + historial = [] + }, + } +} + +const caja = crearAcumulador() +caja.agregar(100) +caja.agregar(250) +caja.agregar(75) +console.log("Total final:", caja.obtenerTotal()) +console.log("Promedio:", caja.promedio()) + +// ============================================= +// Diferencia entre reduce() y Closure +// ============================================= + +const numeros = [10, 20, 30, 40] + +// Con reduce (una sola operación) +const sumaTotal = numeros.reduce((acumulador, numero) => acumulador + numero, 0) +console.log("Suma con reduce:", sumaTotal) + +// Con closure (estado persistente a lo largo del tiempo) +function crearAcumuladorPersistente() { + let suma = 0 + + return (nuevoNumero) => { + suma += nuevoNumero + console.log(`Acumulando... +${nuevoNumero} = ${suma}`) + return suma + } +} + +const acumular = crearAcumuladorPersistente() +acumular(10) +acumular(20) +acumular(30) // mantiene el estado entre llamadas + +// ======================================================== +// patrones-funciones-avanzadas.js +// Archivo educativo completo - Vanilla JS +// Autor: Senior Front-end Fullstack +// ======================================================== + +console.log("%c=== PATRONES DE DISEÑO, CLOSURES, IIFE Y MÓDULOS ===", "color: #00ffcc; font-size: 16px; font-weight: bold") + +/* ============================================================= + 1. IIFE - Immediately Invoked Function Expression + ============================================================= */ +const IIFE = (function () { + // Variables privadas + let contadorPrivado = 0 + const nombreApp = "MiAppVanilla" + + function incrementar() { + contadorPrivado++ + console.log(`[IIFE] Contador privado: ${contadorPrivado}`) + } + + // API pública + return { + saludar: () => console.log(`Hola desde ${nombreApp}`), + incrementarContador: incrementar, + obtenerContador: () => contadorPrivado, + } +})() + +// Uso del IIFE +IIFE.saludar() +IIFE.incrementarContador() +IIFE.incrementarContador() + +/* ============================================================= + 2. CLOSURES + Acumuladores + Estado Persistente + ============================================================= */ + +/** + * Contador con delay (setTimeout) + */ +function crearContadorConDelay() { + let count = 0 + let timer = null + + function incrementarConDelay(ms = 1000) { + if (timer) clearTimeout(timer) + + timer = setTimeout(() => { + count++ + console.log(`%c[Delay] Contador aumentó a: ${count}`, "color: orange") + }, ms) + } + + return { + incrementar: incrementarConDelay, + valor: () => count, + reset: () => { + count = 0 + }, + } +} + +const contadorDelay = crearContadorConDelay() +contadorDelay.incrementar(800) +contadorDelay.incrementar(1500) // Sobrescribe el timer anterior + +/* ============================================================= + 3. MINI ESTADO GLOBAL SEGURO (Pattern muy usado en Vanilla JS) + ============================================================= */ + +const EstadoGlobal = (function () { + // Estado privado (closure) + let state = { + usuario: null, + tema: "light", + notificaciones: 0, + carrito: [], + } + + const listeners = new Set() // Para patrón Observer básico + + function notify() { + listeners.forEach((callback) => callback(state)) + } + + return { + getState: () => ({ ...state }), // Retorna copia + + setUsuario: (usuario) => { + state.usuario = usuario + notify() + console.log("%c[Estado] Usuario actualizado", "color: lime", usuario) + }, + + cambiarTema: (nuevoTema) => { + state.tema = nuevoTema + document.documentElement.setAttribute("data-theme", nuevoTema) + notify() + }, + + agregarAlCarrito: (producto) => { + state.carrito.push(producto) + state.notificaciones++ + notify() + console.log(`Producto agregado: ${producto.nombre}`) + }, + + subscribe: (callback) => { + listeners.add(callback) + // Retorna función para desuscribirse + return () => listeners.delete(callback) + }, + + reset: () => { + state = { usuario: null, tema: "light", notificaciones: 0, carrito: [] } + notify() + }, + } +})() + +// Ejemplo de uso del estado global seguro +EstadoGlobal.subscribe((nuevoEstado) => { + console.log("%c[Observer] Estado cambió:", "color: violet", nuevoEstado) +}) + +EstadoGlobal.setUsuario({ id: 1, nombre: "Yoandy" }) +EstadoGlobal.cambiarTema("dark") +EstadoGlobal.agregarAlCarrito({ id: 101, nombre: "Curso JS Avanzado", precio: 89 }) + +/* ============================================================= + 4. PATRONES DE DISEÑO MÁS USADOS EN VANILLA JS + ============================================================= */ + +// 4.1 Module Pattern (Moderno con IIFE) +const ModuloUsuario = (function () { + let usuarios = [] + + function agregar(usuario) { + usuarios.push(usuario) + console.log(`Usuario agregado: ${usuario.nombre}`) + } + + function listar() { + console.table(usuarios) + return usuarios + } + + function buscarPorId(id) { + return usuarios.find((u) => u.id === id) + } + + // API pública + return { agregar, listar, buscarPorId } +})() + +ModuloUsuario.agregar({ id: 1, nombre: "Yoandy", rol: "Senior" }) + +// 4.2 Factory Pattern +function crearPersona(nombre, edad) { + let _edad = edad // privada + + return { + nombre, + saludar() { + console.log(`Hola, me llamo ${this.nombre} y tengo ${_edad} años`) + }, + cumplirAnios() { + _edad++ + console.log(`${this.nombre} cumplió años! Ahora tiene ${_edad}`) + }, + } +} + +const persona1 = crearPersona("Ana", 28) +persona1.saludar() +persona1.cumplirAnios() + +// 4.3 Singleton Pattern (usando closure) +const SingletonDB = (function () { + let instancia = null + + function crearInstancia() { + return { + conexion: "Activa", + query: (sql) => console.log(`Ejecutando: ${sql}`), + } + } + + return { + getInstance: () => { + if (!instancia) { + instancia = crearInstancia() + console.log("Nueva conexión a BD creada") + } + return instancia + }, + } +})() + +const db1 = SingletonDB.getInstance() +const db2 = SingletonDB.getInstance() +console.log("¿Son la misma instancia?", db1 === db2) + +// 4.4 Revealing Module Pattern +const Calculadora = (function () { + function sumar(a, b) { + return a + b + } + function restar(a, b) { + return a - b + } + function multiplicar(a, b) { + return a * b + } + + // Solo revelamos lo que queremos + return { + suma: sumar, + resta: restar, + multiplicacion: multiplicar, + version: "2.0", + } +})() + +/* ============================================================= + 5. RESUMEN DE CONCEPTOS IMPORTANTES SOBRE FUNCIONES + ============================================================= */ + +console.log("%c=== CONCEPTOS CLAVE SOBRE FUNCIONES ===", "color: #ffcc00; font-weight: bold") + +/* +1. Function Declaration (hoisted) +2. Function Expression +3. Arrow Functions (sin this propio) +4. IIFE +5. Higher-Order Functions (reciben o devuelven funciones) +6. Closures (captura de variables) +7. Currying +8. Partial Application +9. Factory Functions +10. Module Pattern +*/ + +// Ejemplo de Currying +const curriedSuma = (a) => (b) => (c) => a + b + c +console.log("Currying:", curriedSuma(5)(10)(15)) + +// Ejemplo de Higher-Order Function +function conLogger(fn) { + return function (...args) { + console.log(`%c[Llamando función]: ${fn.name}`, "color: cyan") + return fn(...args) + } +} + +const sumaConLog = conLogger((x, y) => x + y) +sumaConLog(7, 8) + +// ======================================================== +// patrones-avanzados-vanilla.js +// Patrones Avanzados, Arquitectura y Buenas Prácticas +// Vanilla JS - Senior Front-end Fullstack +// ======================================================== + +console.log("%c=== PATRONES AVANZADOS EN VANILLA JS ===", "color: #00ff88; font-size: 18px; font-weight: bold") + +/* ============================================================= + 1. EVENT EMITTER PERSONALIZADO (muy útil) + ============================================================= */ +class EventEmitter { + constructor() { + this.events = new Map() + } + + on(eventName, callback) { + if (!this.events.has(eventName)) { + this.events.set(eventName, []) + } + this.events.get(eventName).push(callback) + return () => this.off(eventName, callback) // Retorna unsubscribe + } + + off(eventName, callback) { + if (!this.events.has(eventName)) return + const callbacks = this.events.get(eventName) + this.events.set( + eventName, + callbacks.filter((cb) => cb !== callback), + ) + } + + emit(eventName, ...args) { + if (!this.events.has(eventName)) return + this.events.get(eventName).forEach((callback) => { + try { + callback(...args) + } catch (error) { + console.error(`Error en evento ${eventName}:`, error) + } + }) + } + + once(eventName, callback) { + const wrapper = (...args) => { + callback(...args) + this.off(eventName, wrapper) + } + this.on(eventName, wrapper) + } +} + +// Uso del EventEmitter +const emitter = new EventEmitter() + +const unsubscribe = emitter.on("user:login", (usuario) => { + console.log(`%c[Evento] Usuario inició sesión: ${usuario.nombre}`, "color: gold") +}) + +emitter.emit("user:login", { nombre: "Yoandy", rol: "Senior" }) +unsubscribe() // Desuscribirse + +/* ============================================================= + 2. PUB/SUB PATTERN (Publicador / Suscriptor) + ============================================================= */ +const PubSub = (function () { + const topics = new Map() + let id = 0 + + return { + subscribe: (topic, listener) => { + if (!topics.has(topic)) topics.set(topic, []) + + const subscriberId = ++id + topics.get(topic).push({ id: subscriberId, listener }) + + return () => { + const listeners = topics.get(topic) + topics.set( + topic, + listeners.filter((sub) => sub.id !== subscriberId), + ) + } + }, + + publish: (topic, data) => { + if (!topics.has(topic)) return + topics.get(topic).forEach((subscriber) => { + try { + subscriber.listener(data) + } catch (e) { + console.error(`Error en topic ${topic}:`, e) + } + }) + }, + } +})() + +// Ejemplo Pub/Sub +PubSub.subscribe("cart:updated", (carrito) => { + console.log(`%c[PubSub] Carrito actualizado → ${carrito.length} productos`, "color: hotpink") +}) + +PubSub.publish("cart:updated", [ + { id: 1, name: "Curso JS" }, + { id: 2, name: "Tailwind" }, +]) + +/* ============================================================= + 3. DECORADORES (Decorator Pattern) + ============================================================= */ +function logExecution(targetFunction, functionName = targetFunction.name) { + return function (...args) { + console.time(functionName) + const result = targetFunction(...args) + console.timeEnd(functionName) + console.log(`%c[Decorador] ${functionName} ejecutado con args:`, "color: cyan", args) + return result + } +} + +function debounce(fn, delay = 300) { + let timeout + return function (...args) { + clearTimeout(timeout) + timeout = setTimeout(() => fn(...args), delay) + } +} + +// Uso +const buscar = logExecution((query) => { + console.log(`Buscando: ${query}`) + return `Resultados para ${query}` +}, "buscar") + +buscar("JavaScript Avanzado") + +/* ============================================================= + 4. MEMOIZATION (Optimización de funciones puras) + ============================================================= */ +function memoize(fn) { + const cache = new Map() + + return function (...args) { + const key = JSON.stringify(args) + if (cache.has(key)) { + console.log(`[Memoization] ✅ Cache hit para: ${key}`) + return cache.get(key) + } + + const result = fn(...args) + cache.set(key, result) + console.log(`[Memoization] 📦 Cache miss - Guardado: ${key}`) + return result + } +} + +const fibonacci = memoize(function (n) { + if (n <= 1) return n + return fibonacci(n - 1) + fibonacci(n - 2) +}) + +console.log("Fib(10) =", fibonacci(10)) +console.log("Fib(12) =", fibonacci(12)) // Usará caché para valores anteriores + +/* ============================================================= + 5. EVENT DELEGATION (Gestión avanzada de eventos) + ============================================================= */ +function initEventDelegation() { + const container = document.createElement("div") + container.id = "dynamic-container" + document.body.appendChild(container) + + // Event Delegation - Un solo listener para muchos elementos + container.addEventListener("click", function (e) { + if (e.target.matches(".btn-delete")) { + console.log("Eliminar elemento:", e.target.dataset.id) + e.target.closest(".item").remove() + } + + if (e.target.matches(".btn-edit")) { + console.log("Editar elemento:", e.target.dataset.id) + } + }) + + // Agregar elementos dinámicamente + container.innerHTML = ` +
+

Producto 1

+ + +
+ ` +} + +initEventDelegation() + +/* ============================================================= + 6. ESTRUCTURA DE PROYECTO GRANDE EN VANILLA JS + ============================================================= */ + +/* +Estructura recomendada (Scalable Vanilla JS Architecture): + +/proyecto-vanilla/ +├── index.html +├── /assets/ +│ ├── css/ +│ └── images/ +├── /js/ +│ ├── /core/ ← Base del framework +│ │ ├── eventEmitter.js +│ │ ├── pubsub.js +│ │ ├── store.js +│ │ └── utils.js +│ ├── /modules/ ← Módulos por dominio +│ │ ├── auth/ +│ │ │ ├── auth.module.js +│ │ │ └── auth.ui.js +│ │ ├── cart/ +│ │ ├── products/ +│ │ └── ui/ +│ ├── /components/ ← Web Components o Custom Elements +│ ├── /services/ ← API calls +│ └── main.js ← Entry point +├── /utils/ +└── package.json (opcional con Vite/Parcel) +*/ + +// Ejemplo de main.js (Entry Point) +/* +const App = (function () { + let initialized = false + + function init() { + if (initialized) return + + console.log("%c🚀 Aplicación Vanilla JS inicializada", "color: lime; font-size: 14px") + + // Inicializar módulos + // AuthModule.init(); + // CartModule.init(); + // EventEmitter.global = new EventEmitter(); + + initialized = true + } + + return { init } +})() + +// Auto-inicialización cuando el DOM esté listo +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", App.init) +} else { + App.init() +} + +// ======================================================== +// app-avanzada-vanilla.js +// Mini Aplicación Completa: ToDo + Carrito +// Web Components + Proxy State + Vanilla Router +// Senior Front-end Fullstack +// ======================================================== + +console.log("%c🚀 App Avanzada Vanilla JS - Iniciada", "color:#00ffcc; font-size:18px; font-weight:bold") + +/* ============================================================= + 1. STATE MANAGEMENT AVANZADO CON PROXY + ============================================================= */ +const createStore = (initialState) => { + let state = { ...initialState } + const listeners = new Set() + + const proxy = new Proxy(state, { + set(target, property, value) { + target[property] = value + notify() + return true + }, + }) + + function notify() { + listeners.forEach((listener) => listener(proxy)) + } + + return { + state: proxy, + subscribe: (listener) => { + listeners.add(listener) + listener(proxy) // Llamada inicial + return () => listeners.delete(listener) + }, + setState: (newState) => { + Object.assign(state, newState) + notify() + }, + } +} + +// Store global de la aplicación +const appStore = createStore({ + todos: [], + cart: [], + route: "home", + theme: "light", +}) + +/* ============================================================= + 2. WEB COMPONENTS + CUSTOM ELEMENTS + ============================================================= */ + +// Componente: Todo Item +class TodoItem extends HTMLElement { + constructor() { + super() + this.attachShadow({ mode: "open" }) + } + + connectedCallback() { + this.render() + this.addEventListeners() + } + + set todo(data) { + this._todo = data + if (this.shadowRoot) this.render() + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ + ${this._todo.text} + +
+ ` + } + + addEventListeners() { + const checkbox = this.shadowRoot.querySelector("input") + const deleteBtn = this.shadowRoot.querySelector(".delete") + + checkbox.addEventListener("change", () => { + window.dispatchEvent(new CustomEvent("todo:toggle", { detail: this._todo.id })) + }) + + deleteBtn.addEventListener("click", () => { + window.dispatchEvent(new CustomEvent("todo:delete", { detail: this._todo.id })) + }) + } +} + +customElements.define("todo-item", TodoItem) + +// Componente: Cart Item +class CartItem extends HTMLElement { + connectedCallback() { + this.render() + } + + set item(data) { + this._item = data + this.render() + } + + render() { + this.innerHTML = ` +
+ ${this._item.name} × ${this._item.quantity} + \[ {(this._item.price * this._item.quantity).toFixed(2)} +
+ ` + } +} + +customElements.define("cart-item", CartItem) + +/* ============================================================= + 3. VANILLA ROUTER + ============================================================= */ +class VanillaRouter { + constructor() { + this.routes = new Map() + window.addEventListener("popstate", () => this.handleRoute()) + } + + addRoute(path, callback) { + this.routes.set(path, callback) + } + + navigate(path) { + history.pushState({}, "", path) + this.handleRoute() + } + + handleRoute() { + const path = window.location.pathname || "/" + const handler = this.routes.get(path) || this.routes.get("*") + if (handler) handler() + } +} + +const router = new VanillaRouter() + +router.addRoute("/", () => appStore.setState({ route: "home" })) +router.addRoute("/cart", () => appStore.setState({ route: "cart" })) +router.addRoute("*", () => appStore.setState({ route: "home" })) + +/* ============================================================= + 4. MINI APLICACIÓN - LÓGICA PRINCIPAL + ============================================================= */ + +const App = { + init() { + this.bindEvents() + this.render() + router.handleRoute() + }, + + bindEvents() { + // Eventos globales + window.addEventListener("todo:add", (e) => { + const todos = [ + ...appStore.state.todos, + { + id: Date.now(), + text: e.detail, + completed: false, + }, + ] + appStore.setState({ todos }) + }) + + window.addEventListener("todo:toggle", (e) => { + const todos = appStore.state.todos.map((todo) => (todo.id === e.detail ? { ...todo, completed: !todo.completed } : todo)) + appStore.setState({ todos }) + }) + + window.addEventListener("todo:delete", (e) => { + const todos = appStore.state.todos.filter((todo) => todo.id !== e.detail) + appStore.setState({ todos }) + }) + + // Suscripción al store + appStore.subscribe(() => this.render()) + }, + + addTodo(text) { + window.dispatchEvent(new CustomEvent("todo:add", { detail: text })) + }, + + addToCart(product) { + const cart = [...appStore.state.cart] + const existing = cart.find((item) => item.id === product.id) + + if (existing) { + existing.quantity += 1 + } else { + cart.push({ ...product, quantity: 1 }) + } + + appStore.setState({ cart }) + }, + + render() { + const { todos, cart, route } = appStore.state + + // Renderizar según ruta (SPA simple) + if (route === "cart") { + this.renderCart(cart) + } else { + this.renderHome(todos) + } + }, + + renderHome(todos) { + const container = document.getElementById("app-container") + if (!container) return + + container.innerHTML = ` +

ToDo + Carrito (Vanilla JS Avanzado)

+ + + + +

Tareas (${todos.length})

+
+ +

Productos

+ + + +

Ir al Carrito (${cart.length} productos)

+ ` + + // Renderizar todos con Web Component + const todoList = document.getElementById("todo-list") + todos.forEach((todo) => { + const todoEl = document.createElement("todo-item") + todoEl.todo = todo + todoList.appendChild(todoEl) + }) + + // Evento agregar todo + document.getElementById("add-todo").addEventListener("click", () => { + const input = document.getElementById("todo-input") + if (input.value.trim()) { + this.addTodo(input.value.trim()) + input.value = "" + } + }) + }, + + renderCart(cart) { + const container = document.getElementById("app-container") + const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0) + + container.innerHTML = ` +

🛒 Carrito de Compras

+
+

Total: \]{total.toFixed(2)}

+ + ` + + const cartContainer = document.getElementById("cart-items") + cart.forEach((item) => { + const cartEl = document.createElement("cart-item") + cartEl.item = item + cartContainer.appendChild(cartEl) + }) + }, +} + +/* ============================================================= + 5. PERFORMANCE & BUENAS PRÁCTICAS + ============================================================= */ + +// Lazy Loading de imágenes (ejemplo) +function lazyLoadImages() { + const images = document.querySelectorAll("img[data-src]") + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target + img.src = img.dataset.src + observer.unobserve(img) + } + }) + }) + + images.forEach((img) => observer.observe(img)) +} + +// Debounce para búsqueda +const debounce = (fn, delay) => { + let timeout + return (...args) => { + clearTimeout(timeout) + timeout = setTimeout(() => fn(...args), delay) + } +} + +/* ============================================================= + 6. TESTING BÁSICO (Jest-like) + ============================================================= */ + +const assert = (condition, message) => { + if (!condition) throw new Error(message || "Test failed") +} + +const test = (name, fn) => { + try { + fn() + console.log(`✅ ${name}`) + } catch (e) { + console.error(`❌ ${name}: ${e.message}`) + } +} + +// Tests +test("Store debería actualizar estado", () => { + const testStore = createStore({ count: 0 }) + testStore.setState({ count: 5 }) + assert(testStore.state.count === 5) +}) + +test("Router debería navegar", () => { + router.navigate("/cart") + assert(appStore.state.route === "cart") +}) + +/* ============================================================= + 7. INICIALIZACIÓN + ============================================================= */ + +document.addEventListener("DOMContentLoaded", () => { + App.init() + lazyLoadImages() + + console.log("%c✅ Aplicación completa cargada correctamente", "color:lime; font-weight:bold") +}) + +// Exponer App globalmente para botones inline +window.App = App +window.router = router diff --git a/fullstack/javascript/3-functions/truncate-string-algorithm-lab.js b/fullstack/javascript/3-functions/truncate-string-algorithm-lab.js new file mode 100644 index 0000000..394364e --- /dev/null +++ b/fullstack/javascript/3-functions/truncate-string-algorithm-lab.js @@ -0,0 +1,23 @@ +/** + * Function thath truncate a string to a certain length. If the length of the string is more than the given number, the string should be truncated to reduce the length so that it is equal the given number, and ... If the length of the string is equal to or lower than the given number, the string should be returned unchanged. + * @param {String} sentence any sentence. + * @param {Number} maxLength maximun length. + * @returns {String} a sentence + */ +function truncateString(sentence, maxLength) { + if (sentence.length <= maxLength) { + return sentence + } else { + let truncated = sentence.slice(0, maxLength) + return truncated + "..." + } +} + +console.log(truncateString("A-tisket a-tasket A green and yellow basket", 8)) +console.log(truncateString("Peter Piper picked a peck of pickled peppers", 11)) +console.log(truncateString("A-tisket a-tasket A green and yellow basket", "A-tisket a-tasket A green and yellow basket".length)) +console.log( + truncateString("A-tisket a-tasket A green and yellow basket", "A-tisket a-tasket A green and yellow basket".length + 2), +) +console.log(truncateString("A-", 1)) +console.log(truncateString("Absolutely Longer", 2)) diff --git a/fullstack/javascript/3-functions/truncate-string-algorithm-lab.md b/fullstack/javascript/3-functions/truncate-string-algorithm-lab.md new file mode 100644 index 0000000..ce08d89 --- /dev/null +++ b/fullstack/javascript/3-functions/truncate-string-algorithm-lab.md @@ -0,0 +1,11 @@ +# Implement the Truncate a String Algorithm + +In this lab, you will practice truncating a string to a certain length. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function truncateString that accepts two arguments, the first one a string, the second one a number. +2. [x] If the length of the string is more than the given number, the string should be truncated to reduce the length so that it is equal the given number, and ... should be appended at the end of the truncated string. +3. [x] If the length of the string is equal to or lower than the given number, the string should be returned unchanged. diff --git a/fullstack/javascript/4-array/exam/functionr-arrays-rebuild.js b/fullstack/javascript/4-array/exam/functionr-arrays-rebuild.js new file mode 100644 index 0000000..d325dcd --- /dev/null +++ b/fullstack/javascript/4-array/exam/functionr-arrays-rebuild.js @@ -0,0 +1,202 @@ +// ════════════════════════════════════════ +// PART 3 — FUNCTIONS DEEP DIVE (25 pts) +// ════════════════════════════════════════ + +/** + * Write a function memoize(fn) that takes any function and returns a new function that caches its results. If called again with the same argument, return the cached result without calling fn again. + * const slowDouble = n => { ...some slow computation... return n * 2 } + * const fastDouble = memoize(slowDouble) + * fastDouble(5) → 10 (computed) + * fastDouble(5) → 10 (from cache — fn not called again) + * fastDouble(3) → 6 (computed) + */ +// Your solution: + +/** + * Function memoize(fn) that takes any function and returns a new function that caches its results. + * @param {Function} fn any function. + * @returns returns a new function that caches its results. + */ +const memoize = (fn) => { + // 1. Create empty cache - map + let cache = new Map() + return (num) => { + if (!cache.has(num)) { + const result = fn(num) + cache.set(num, result) + return result + } else { + return cache.get(num) + } + } +} + +/** + * Function that given a number pow by 2. + * @param {Number} n any number. + * @returns {Number} numbur duplicated. + */ +const slowDouble = (n) => { + return n * 2 +} +const fastDouble = memoize(slowDouble) +// --- Test Cases --- +console.log("--- Challenge 1 Running Tests ---") +console.log(fastDouble(5)) // expected: 10 (computed) +console.log(fastDouble(5)) // expected: 10 (from cache — fn not called again) +console.log(fastDouble(3)) // expected: 6 (computed) +console.log("\n====================================") + +// ── F3 (5 pts) ─────────────────────────── +/** + * Write a function curry(fn) that transforms a function of N arguments + * into a chain of N single-argument functions. + * + * const add = (a, b, c) => a + b + c + * const curriedAdd = curry(add) + * curriedAdd(1)(2)(3) → 6 + * curriedAdd(10)(20)(30) → 60 + * + * Hint: check fn.length to know how many arguments are expected. + * Think recursion — if not enough args collected yet, return another function. + */ +// Your solution: + +/** + * Function that transforms a function of N arguments + * into a chain of N single-argument functions. + * @param {Function} fn Any function with N arguments + */ +const curry = (fn) => { + return function curried(...args) { + if (args.length >= fn.length) { + return fn(...args) + } else { + return (...arg) => curried(...args, ...arg) + } + } +} + +// --- Test Cases --- +console.log("--- Challenge 3 Running Tests ---") +const add = (a, b, c) => a + b + c +const curriedAdd = curry(add) +console.log(curriedAdd(1)(2)(3)) // 6 — one arg at a time +console.log(curriedAdd(10)(20)(30)) // 60 +console.log(curriedAdd(1, 2)(3)) // 6 — partial application also works +console.log(curriedAdd(1)(2, 3)) // 6 +console.log(curriedAdd(1, 2, 3)) // 6 — all at once still works +console.log("\n====================================") + +// ── F4 (4 pts) ─────────────────────────── +/** + * Write a function once(fn) that ensures fn is only called ONCE. + * Every subsequent call returns the result of the first call + * without executing fn again. + * + * let count = 0 + * const increment = once(() => ++count) + * increment() → 1 + * increment() → 1 (not 2 — fn not called again) + * increment() → 1 + * console.log(count) → 1 + */ +// Your solution: + +/** + * Function that ensures fn is only called ONCE. + * @param {Function} fn any function. + * @returns {Function} result of the first call without executing fn again. + */ +const once = (fn) => { + let called = false + let result + return () => { + if (!called) { + result = fn() + called = true + } + return result + } +} + +// --- Test Cases --- +console.log("--- Challenge 4 Running Tests ---") +let count = 0 +const increment = once(() => ++count) +increment() //→ 1 +console.log(count) //→ 1 +increment() //→ 1 (not 2 — fn not called again) +increment() //→ 1 +console.log(count) //→ 1 +console.log("\n====================================") + +// Real-world use: initialise something expensive only once +const initDB = once(() => { + console.log(" DB connection opened (should appear only once)") + return { connected: true } +}) +console.log(initDB()) // opens connection, returns { connected: true } +console.log(initDB()) // skips, returns same object +console.log(initDB()) // skips, returns same object + +// ── X1 (6 pts) ─────────────────────────── +/** + * Implement a pipeline function that processes a dataset of products. + * + * Given this array of products: + */ +const products = [ + { name: "Laptop", price: 1200, category: "tech", stock: 5 }, + { name: "Phone", price: 800, category: "tech", stock: 0 }, + { name: "Desk", price: 350, category: "furniture", stock: 3 }, + { name: "Monitor", price: 600, category: "tech", stock: 2 }, + { name: "Chair", price: 250, category: "furniture", stock: 7 }, + { name: "Tablet", price: 400, category: "tech", stock: 0 }, + { name: "Keyboard", price: 150, category: "tech", stock: 10 }, +] +/** + * Using ONLY array methods (no for loops), return: + * { + * available: [...], // products with stock > 0, sorted by price descending + * byCategory: { tech: [...], furniture: [...] }, // grouped by category (names only) + * totalValue: number, // sum of (price * stock) for available products only + * cheapest: object, // the cheapest available product (full object) + * } + */ +// Your solution: + +/** + * Function that processes a dataset of products. + * @param {Array} arr any array of product objects. + * @returns {Object} processed dataset of products. + */ +const pipeline = (arr) => { + // 1. available: [...], // products with stock > 0, sorted by price descending + const available = arr.filter((products) => products.stock > 0).sort((a, b) => b.price - a.price) + // 2. byCategory: { tech: [...], furniture: [...] }, // grouped by category (names only) + const byCategory = arr.reduce((acc, product) => { + if (!acc[product.category]) { + acc[product.category] = [] + } + acc[product.category].push(product.name) + return acc + }, {}) + // 3. totalValue: number, // sum of (price * stock) for available products only + const totalValue = available.reduce((acc, products) => acc + products.price * products.stock, 0) + // 4. cheapest: object, // the cheapest available product (full object) + const cheapest = available[available.length - 1] + return { available, byCategory, totalValue, cheapest } +} + +console.log("--- X1: pipeline ---") +const result = pipeline(products) +console.log( + "available:", + result.available.map((p) => p.name), +) +// ['Laptop','Monitor','Desk','Chair','Keyboard'] +console.log("byCategory:", result.byCategory) +// { tech: ['Laptop','Phone','Monitor','Tablet','Keyboard'], furniture: ['Desk','Chair'] } +console.log("totalValue:", result.totalValue) // 11500 +console.log("cheapest:", result.cheapest) // { name: 'Keyboard', ... } diff --git a/fullstack/javascript/4-array/exam/functions-arrays-exam-claude-solutions.js b/fullstack/javascript/4-array/exam/functions-arrays-exam-claude-solutions.js new file mode 100644 index 0000000..50cb316 --- /dev/null +++ b/fullstack/javascript/4-array/exam/functions-arrays-exam-claude-solutions.js @@ -0,0 +1,736 @@ +/** + * ======================================== + * FUNCTIONS & ARRAYS MASTERY EXAM — SOLUTIONS + * ======================================== + * Student: Yoandy Doble Herrera + * Reviewed by: Claude + * Score: 75/100 → All bugs fixed below + * + * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + * THE CORE INSIGHT — READ THIS FIRST + * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + * + * A CLOSURE is a function that remembers variables from the + * scope where it was created — even after that scope is gone. + * + * You already proved you understand this with pipe(): + * + * const pipe = (...fns) => + * fns.reduce((fn, nextFn) => (x) => nextFn(fn(x)), (x) => x) + * + * The inner (x) => nextFn(fn(x)) closes over fn and nextFn. + * Those variables live on inside the returned function. + * That IS a closure. You wrote it perfectly. + * + * Now here is the pattern that unlocks everything: + * + * const somePattern = (config) => { + * // 1. Declare something to REMEMBER across calls + * let memory = ... + * + * // 2. Return a function that USES that memory + * return (...args) => { + * // read or update memory here + * } + * } + * + * Every advanced function pattern is just this skeleton + * with a different "thing to remember": + * + * memoize → remembers a MAP of previous results + * curry → remembers COLLECTED ARGS until there are enough + * once → remembers a CALLED FLAG + the cached RESULT + * debounce → remembers a TIMER ID to cancel/reset + * + * That's it. Same skeleton, four different memories. + * Read each solution below with this in mind. + */ + +// ════════════════════════════════════════ +// PART 1 — THEORY & PREDICTIONS +// ════════════════════════════════════════ +// All predictions were correct (22/25). +// No code changes needed — just a note on Q7. + +console.log("\n=== Part 1 — Theory & Predictions ===") + +// Q1 — Q6: all correct ✓ + +// Q7: mystery() — the precise one-liner description: +// "Groups array elements into an object keyed by the return value of fn." +// This pattern is called groupBy — you'll use it constantly in React +// when grouping data for rendering (e.g. orders by status, posts by category). + +const mystery = (arr, fn) => + arr.reduce((acc, val) => { + const key = fn(val) + if (!acc[key]) acc[key] = [] + acc[key].push(val) + return acc + }, {}) + +const people = ["Alice", "Bob", "Anna", "Brian", "Charlie"] +const grouped = mystery(people, (name) => name[0]) +console.log(grouped) // { A: ['Alice','Anna'], B: ['Bob','Brian'], C: ['Charlie'] } +console.log(grouped["A"]) // ['Alice', 'Anna'] +console.log(grouped["B"].length) // 2 + +// ════════════════════════════════════════ +// PART 2 — ARRAY METHODS +// ════════════════════════════════════════ + +console.log("\n=== Part 2 — Array Methods ===") + +// ── C1 — unique (4/4) ✓ ───────────────── +// Your solution was correct. No changes. +/** + * Returns a new array with only unique values, preserving order of first appearance. + * @param {Array} arr array of integers. + * @returns {Array} deduplicated array. + */ +const unique = (arr) => + arr.reduce((acc, item) => { + if (!acc.includes(item)) acc.push(item) + return acc + }, []) + +console.log("--- C1: unique ---") +console.log(unique([1, 2, 1, 3, 2, 4])) // [1, 2, 3, 4] +console.log(unique([5, 5, 5])) // [5] +console.log(unique([])) // [] + +// ── C2 — countOccurrences (4/4) ✓ ─────── +// Your solution was correct. No changes. +/** + * Counts occurrences of each element in an array. + * @param {Array} arr array of strings or numbers. + * @returns {Object} { element: count } + */ +const countOccurrences = (arr) => + arr.reduce((acc, val) => { + acc[val] = (acc[val] || 0) + 1 + return acc + }, {}) + +console.log("\n--- C2: countOccurrences ---") +console.log(countOccurrences(["a", "b", "a", "c", "b", "a"])) // { a:3, b:2, c:1 } +console.log(countOccurrences([])) // {} + +// ── C3 — deepFlat (4/5) → Fixed ───────── +// BUG: typeof num === "number" breaks for arrays of strings/booleans. +// FIX: use Array.isArray() — if it's an array, recurse; otherwise push. +/** + * Flattens a nested array of any depth without using Array.flat(). + * @param {Array} arr nested array of any depth. + * @returns {Array} fully flattened array. + */ +const deepFlat = (arr) => + arr.reduce((acc, item) => { + if (Array.isArray(item)) { + // item is a nested array — recurse and spread results in + acc.push(...deepFlat(item)) + } else { + // item is a leaf value (number, string, boolean, etc.) + acc.push(item) + } + return acc + }, []) + +console.log("\n--- C3: deepFlat ---") +console.log(deepFlat([1, [2, [3, [4]], 5]])) // [1,2,3,4,5] +console.log(deepFlat([1, 2, 3])) // [1,2,3] +console.log(deepFlat([])) // [] +console.log(deepFlat(["a", ["b", ["c"]]])) // ['a','b','c'] — now works for strings too +console.log(deepFlat([true, [false, [true]]])) // [true,false,true] — booleans too + +// ── C4 — graduates (4/4) ✓ ────────────── +// Your solution was correct and well-tested. No changes. +/** + * Returns students who passed (score >= 60), sorted by score descending, + * with only name and score properties. + * @param {Array} arr array of student objects. + * @returns {Array} filtered, sorted, projected array. + */ +const graduates = (arr) => + arr + .filter((s) => s.score >= 60) + .map((s) => ({ name: s.name, score: s.score })) + .sort((a, b) => b.score - a.score) + +const students = [ + { name: "Alice", score: 90, age: 20 }, + { name: "Bob", score: 45, age: 22 }, + { name: "Carol", score: 75, age: 19 }, + { name: "Dave", score: 60, age: 21 }, +] +console.log("\n--- C4: graduates ---") +console.log(graduates(students)) +// [{ name:'Alice', score:90 }, { name:'Carol', score:75 }, { name:'Dave', score:60 }] + +// ── C5 — myMap (5/5) ✓ ────────────────── +// Your solution was correct. No changes. +/** + * Custom implementation of Array.prototype.map using for...of. + * @param {Array} arr any array. + * @param {Function} fnc transformation function. + * @returns {Array} mapped array. + */ +const myMap = (arr, fnc) => { + const result = [] + for (const element of arr) { + result.push(fnc(element)) + } + return result +} + +console.log("\n--- C5: myMap ---") +console.log(myMap([1, 2, 3], (x) => x * 2)) // [2,4,6] +console.log(myMap(["a", "b"], (s) => s.toUpperCase())) // ['A','B'] +console.log(myMap([], (x) => x)) // [] + +// ── C6 — stats (2/4) → Fixed ──────────── +// BUG 1: Math.floor instead of toFixed(2) — lost decimal precision. +// BUG 2: Math.min/Math.max called outside reduce — two extra passes. +// FIX: compute everything inside a single reduce. +// Use Infinity/-Infinity as initial min/max. +// Calculate avg on the last iteration using the index + array reference. +/** + * Returns min, max, avg (2dp), and sum of a number array in a single reduce pass. + * @param {Array} arr array of numbers. + * @returns {Object} { min, max, avg, sum } + */ +const stats = (arr) => { + if (arr.length === 0) return {} + + return arr.reduce( + (acc, n, i, src) => { + // running sum — same as before + acc.sum += n + // in-reduce min/max — no separate Math.min/max pass needed + if (n < acc.min) acc.min = n + if (n > acc.max) acc.max = n + // compute avg only on the final iteration (i === last index) + if (i === src.length - 1) { + acc.avg = parseFloat((acc.sum / src.length).toFixed(2)) + } + return acc + }, + { min: Infinity, max: -Infinity, sum: 0, avg: 0 }, + ) +} + +console.log("\n--- C6: stats ---") +console.log(stats([3, 1, 4, 1, 5, 9])) // { min:1, max:9, avg:3.83, sum:23 } ✓ +console.log(stats([10])) // { min:10, max:10, avg:10, sum:10 } +console.log(stats([12, -5, 33, 10])) // { min:-5, max:33, avg:12.5, sum:50 } +console.log(stats([])) // {} + +// ── C7 — sameElements (4/4) ✓ ─────────── +// Your Map-based approach is optimal O(n). No changes. +/** + * Returns true if both arrays contain the same elements (order-independent, duplicates matter). + * @param {Array} arrOne any array. + * @param {Array} arrTwo any array. + * @returns {Boolean} + */ +const arrToMap = (arr) => { + const map = new Map() + for (const item of arr) { + map.set(item, (map.get(item) || 0) + 1) + } + return map +} + +const sameElements = (arrOne, arrTwo) => { + if (arrOne.length !== arrTwo.length) return false + const mapOne = arrToMap(arrOne) + const mapTwo = arrToMap(arrTwo) + for (const [key, value] of mapOne.entries()) { + if (mapTwo.get(key) !== value) return false + } + return true +} + +console.log("\n--- C7: sameElements ---") +console.log(sameElements([1, 2, 3], [3, 2, 1])) // true +console.log(sameElements([1, 2, 2], [2, 1, 2])) // true +console.log(sameElements([1, 2, 3], [1, 2, 4])) // false +console.log(sameElements([1, 2], [1, 2, 3])) // false + +// ════════════════════════════════════════ +// PART 3 — FUNCTIONS DEEP DIVE +// ════════════════════════════════════════ +// This is where the closure skeleton applies. +// Read the comment before each function carefully. + +console.log("\n=== Part 3 — Functions Deep Dive ===") + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// THE CLOSURE SKELETON — keep this in mind: +// +// const pattern = (config) => { +// let MEMORY = ... ← what to remember +// return (...args) => { +// // use or update MEMORY +// } +// } +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +// ── F1 — memoize (0/4) → Fixed ────────── +// YOUR VERSION: (x) => fn(x) — just delegates, no memory at all. +// +// CLOSURE MEMORY: a Map of { argument → result } +// When called with an argument we've seen → return from Map (fn NOT called) +// When called with a new argument → call fn, store result in Map, return it +// +// WHY MAP? Because Map keys can be any type (numbers, objects, etc.) +// A plain object would stringify keys, breaking for non-string args. +/** + * Returns a memoized version of fn that caches results by argument. + * Subsequent calls with the same argument return the cached value + * without invoking fn again. + * @param {Function} fn any single-argument function. + * @returns {Function} memoized function. + */ +const memoize = (fn) => { + // MEMORY: Map of previously seen argument → result pairs + const cache = new Map() + + return (x) => { + if (cache.has(x)) { + console.log(` [cache hit] ${x}`) + return cache.get(x) // return stored result — fn NOT called + } + console.log(` [computing] ${x}`) + const result = fn(x) + cache.set(x, result) // store for future calls + return result + } +} + +const slowDouble = (n) => n * 2 +const fastDouble = memoize(slowDouble) + +console.log("--- F1: memoize ---") +console.log(fastDouble(5)) // [computing] 10 +console.log(fastDouble(5)) // [cache hit] 10 — fn NOT called again +console.log(fastDouble(3)) // [computing] 6 +console.log(fastDouble(5)) // [cache hit] 10 +// Notice: "computing" only appears twice (for 5 and 3), not three times + +// ── F2 — pipe (4/4) ✓ ─────────────────── +// Your solution was PERFECT. This is exactly how pipe is written in production. +// The reduce accumulates a composed function, starting with identity (x) => x. +// Each step wraps the previous result: (x) => nextFn(fn(x)) +// This IS a closure — the inner arrow closes over fn and nextFn. +/** + * Composes functions left to right. Output of each becomes input of the next. + * @param {...Function} fns functions to compose. + * @returns {Function} composed function. + */ +const pipe = (...fns) => + fns.reduce( + (fn, nextFn) => (x) => nextFn(fn(x)), + (x) => x, // identity: starting point + ) + +const process = pipe( + (x) => x * 2, + (x) => x + 1, + (x) => x ** 2, +) + +console.log("\n--- F2: pipe ---") +console.log(process(3)) // ((3*2)+1)^2 = 49 +console.log(process(0)) // ((0*2)+1)^2 = 1 +console.log(process(15)) // ((15*2)+1)^2 = 961 + +// ── F3 — curry (0/5) → Fixed ──────────── +// YOUR VERSION: (...args) => fn(...args) — just calls fn immediately with all args. +// curriedAdd(1) returns NaN (1+undefined+undefined), not a function. Can't chain. +// +// CLOSURE MEMORY: the COLLECTED ARGS so far +// Each call adds more args to the collection. +// When we have enough (args.length >= fn.length), call fn. +// Otherwise return another function that collects more. +// +// KEY INSIGHT: fn.length is the number of parameters fn declares. +// const add = (a, b, c) => a + b + c → add.length === 3 +// curry knows to wait until it has 3 args total before calling. +/** + * Transforms a multi-argument function into a chain of single-argument functions. + * Supports partial application: curriedAdd(1)(2)(3) or curriedAdd(1,2)(3). + * @param {Function} fn function with fn.length declared parameters. + * @returns {Function} curried version of fn. + */ +const curry = (fn) => { + const arity = fn.length // how many args fn needs (e.g. 3 for add(a,b,c)) + + // MEMORY: the args collected so far (grows with each call) + return function curried(...args) { + if (args.length >= arity) { + // Have enough args — call the original function + return fn(...args) + } + // Not enough yet — return a function that collects more + // The new call's args are merged with previously collected args + return (...moreArgs) => curried(...args, ...moreArgs) + // ^^^^^ closure over previously collected args + } +} + +const add = (a, b, c) => a + b + c +const curriedAdd = curry(add) + +console.log("\n--- F3: curry ---") +console.log(curriedAdd(1)(2)(3)) // 6 — one arg at a time +console.log(curriedAdd(10)(20)(30)) // 60 +console.log(curriedAdd(1, 2)(3)) // 6 — partial application also works +console.log(curriedAdd(1)(2, 3)) // 6 +console.log(curriedAdd(1, 2, 3)) // 6 — all at once still works + +// Another example to feel it: +const multiply = (a, b) => a * b +const curriedMultiply = curry(multiply) +const double = curriedMultiply(2) // partially applied — "double" is now a function +const triple = curriedMultiply(3) // partially applied — "triple" is now a function +console.log(double(5)) // 10 +console.log(triple(5)) // 15 +// This is the factory pattern from ch5-study.js — same idea, expressed differently + +// ── F4 — once (2/4) → Fixed ───────────── +// YOUR VERSION: if (++iterator === 1) fn() — logic correct but return value lost. +// fn() was called but its return value was discarded. increment() returned undefined. +// +// CLOSURE MEMORY: a CALLED FLAG (boolean) + the CACHED RESULT +// First call: run fn, store result in memory, set flag to true, return result +// Every subsequent call: flag is true → skip fn, return cached result directly +/** + * Wraps fn so it only executes once. All subsequent calls return the first result. + * @param {Function} fn any function. + * @returns {Function} once-guarded version of fn. + */ +const once = (fn) => { + // MEMORY: whether fn has been called + what it returned + let called = false + let result + + return (...args) => { + if (!called) { + called = true + result = fn(...args) // store the return value + } + return result // always return it (cached after first call) + } +} + +let count = 0 +const increment = once(() => ++count) + +console.log("\n--- F4: once ---") +console.log(increment()) // 1 — fn runs, count becomes 1 +console.log(increment()) // 1 — fn does NOT run, returns cached 1 +console.log(increment()) // 1 — same +console.log(count) // 1 — count was only incremented once ✓ + +// Real-world use: initialise something expensive only once +const initDB = once(() => { + console.log(" DB connection opened (should appear only once)") + return { connected: true } +}) +console.log(initDB()) // opens connection, returns { connected: true } +console.log(initDB()) // skips, returns same object +console.log(initDB()) // skips, returns same object + +// ── F5 — flattenRecursive + flattenReduce (4/4) ✓ ──── +// Both of your solutions were correct. Reproduced with minor cleanup. + +/** + * Flattens array recursively using head/rest destructuring and spread. + * No loops — pure recursion. + * @param {Array} arr nested array. + * @returns {Array} flat array. + */ +const deepFlatWithSpread = (arr) => { + if (arr.length === 0) return [] + const [first, ...rest] = arr + if (Array.isArray(first)) { + // first is itself an array — flatten it, then continue with rest + return deepFlatWithSpread(first.concat(rest)) + } + // first is a leaf — keep it, recurse on rest + return [first, ...deepFlatWithSpread(rest)] +} + +/** + * Flattens array using reduce + recursion. + * @param {Array} arr nested array. + * @returns {Array} flat array. + */ +const deepFlatWithReduce = (arr) => + arr.reduce((acc, item) => { + Array.isArray(item) ? acc.push(...deepFlatWithReduce(item)) : acc.push(item) + return acc + }, []) + +console.log("\n--- F5: flatten variants ---") +console.log(deepFlatWithSpread([1, [2, [3, [4]], 5]])) // [1,2,3,4,5] +console.log(deepFlatWithReduce([1, [2, [3, [4]], 5]])) // [1,2,3,4,5] +console.log(deepFlatWithSpread([])) // [] + +// ── F6 — debounce (1/4) → Fixed ───────── +// YOUR VERSION: setTimeout(fn(msg), delay) — executes fn immediately. +// setTimeout takes a FUNCTION as first arg, not a function's return value. +// fn(msg) calls fn right away and passes undefined (its return value) to setTimeout. +// +// CLOSURE MEMORY: a TIMER ID +// Each call to the debounced function cancels the previous timer +// and schedules a fresh one. fn only runs if no new call arrives +// before the delay expires. +// +// REAL WORLD: search input — don't hit the API on every keystroke, +// only after the user pauses typing for 300ms. +/** + * Delays fn until after delay ms have passed since the last call. + * If called again before the delay expires, resets the timer. + * @param {Function} fn function to debounce. + * @param {Number} delay milliseconds to wait. + * @returns {Function} debounced function. + */ +const debounce = (fn, delay) => { + // MEMORY: the ID of the currently scheduled timer + let timer + + return (...args) => { + clearTimeout(timer) // cancel any pending call + timer = setTimeout(() => fn(...args), delay) // schedule fresh call + // ^^^ arrow function wraps fn — it runs AFTER delay, not now + } +} + +console.log("\n--- F6: debounce ---") +const log = debounce((msg) => console.log(" fired:", msg), 150) + +// Simulate rapid calls — only the last one should fire +setTimeout(() => log("a"), 0) // scheduled, then cancelled +setTimeout(() => log("b"), 50) // scheduled, then cancelled +setTimeout(() => log("c"), 100) // scheduled, fires after 150ms silence +// Expected output (after ~250ms): " fired: c" + +// Verify: calls that arrive after the delay DO fire independently +setTimeout(() => log("d"), 400) // new burst — fires after 150ms +// Expected output (after ~550ms): " fired: d" + +// ════════════════════════════════════════ +// PART 4 — COMBINED CHALLENGES +// ════════════════════════════════════════ + +console.log("\n=== Part 4 — Combined Challenges ===") + +// ── X1 — pipeline (4/6) → Fixed ───────── +// BUG: byCategory stored full product objects instead of names only. +// FIX: add .map(p => p.name) after grouping each category. +const products = [ + { name: "Laptop", price: 1200, category: "tech", stock: 5 }, + { name: "Phone", price: 800, category: "tech", stock: 0 }, + { name: "Desk", price: 350, category: "furniture", stock: 3 }, + { name: "Monitor", price: 600, category: "tech", stock: 2 }, + { name: "Chair", price: 250, category: "furniture", stock: 7 }, + { name: "Tablet", price: 400, category: "tech", stock: 0 }, + { name: "Keyboard", price: 150, category: "tech", stock: 10 }, +] + +/** + * Processes a product dataset using only array methods. + * Returns available products, category groups (names only), total stock value, and cheapest item. + * @param {Array} arr array of product objects. + * @returns {Object} { available, byCategory, totalValue, cheapest } + */ +const pipeline = (arr) => { + // products with stock > 0, sorted by price descending + const available = arr.filter((p) => p.stock > 0).sort((a, b) => b.price - a.price) + + // group ALL products by category — names only (not full objects) + const byCategory = arr.reduce((acc, p) => { + if (!acc[p.category]) acc[p.category] = [] + acc[p.category].push(p.name) // ← names only, not full object + return acc + }, {}) + + // sum of price * stock for available products only + const totalValue = available.reduce((acc, p) => acc + p.price * p.stock, 0) + + // cheapest available — last item after sorting descending + const cheapest = available[available.length - 1] + + return { available, byCategory, totalValue, cheapest } +} + +console.log("--- X1: pipeline ---") +const result = pipeline(products) +console.log( + "available:", + result.available.map((p) => p.name), +) +// ['Laptop','Monitor','Desk','Chair','Keyboard'] +console.log("byCategory:", result.byCategory) +// { tech: ['Laptop','Phone','Monitor','Tablet','Keyboard'], furniture: ['Desk','Chair'] } +console.log("totalValue:", result.totalValue) // 11500 +console.log("cheapest:", result.cheapest) // { name: 'Keyboard', ... } + +// ── X2 — makeValidator (5/7) → Fixed ──── +// BUG: hardcoded { name, age, email } destructuring — only works for those keys. +// FIX: iterate Object.entries(rules) so it works for ANY rule set. +/** + * Creates a validator from a rules object. + * Each rule is a function that returns true if the field is valid. + * @param {Object} rules { fieldName: validatorFn } + * @returns {Function} validate(data) → { valid: boolean, errors: string[] } + */ +const makeValidator = (rules) => (data) => { + // iterate rules dynamically — works for any keys, not just name/age/email + const errors = Object.entries(rules) + .filter(([key, ruleFn]) => !ruleFn(data[key])) + .map(([key]) => key) + + return { valid: errors.length === 0, errors } +} + +const validator = makeValidator({ + name: (v) => typeof v === "string" && v.length >= 2, + age: (v) => typeof v === "number" && v >= 18 && v <= 120, + email: (v) => typeof v === "string" && v.includes("@"), +}) + +console.log("\n--- X2: makeValidator ---") +console.log(validator({ name: "Y", age: 25, email: "y@x.com" })) +// { valid: false, errors: ['name'] } +console.log(validator({ name: "Yoandy", age: 25, email: "y@x.com" })) +// { valid: true, errors: [] } +console.log(validator({ name: "Y", age: 15, email: "notanemail" })) +// { valid: false, errors: ['name','age','email'] } + +// Bonus: works with any field set — not locked to name/age/email +const productValidator = makeValidator({ + title: (v) => typeof v === "string" && v.length > 0, + price: (v) => typeof v === "number" && v > 0, + category: (v) => ["tech", "furniture", "clothing"].includes(v), +}) +console.log(productValidator({ title: "", price: -5, category: "food" })) +// { valid: false, errors: ['title','price','category'] } + +// ── X3 — createEmitter (5/7) → Fixed ──── +// BUG: stored [listener, emitted, isOnce] — only one listener per event. +// A second on("say", fn2) would be silently ignored. +// FIX: store an ARRAY of listeners per event. Each listener can be removed +// independently. once() wraps the listener in a self-removing wrapper. +/** + * Creates a simple event emitter supporting on, off, emit, and once. + * Multiple listeners per event are supported. + * @returns {{ on, off, emit, once }} + */ +const createEmitter = () => { + // MEMORY: object mapping event names to arrays of listener functions + const events = {} + + return { + /** + * Subscribes a listener to an event. + * @param {String} event event name. + * @param {Function} listener callback function. + */ + on(event, listener) { + if (!events[event]) events[event] = [] + events[event].push(listener) + }, + + /** + * Unsubscribes a specific listener from an event. + * @param {String} event event name. + * @param {Function} listener the exact function to remove. + */ + off(event, listener) { + if (!events[event]) return + // filter out the specific listener — others remain subscribed + events[event] = events[event].filter((fn) => fn !== listener) + }, + + /** + * Calls all listeners for the event with the given args. + * @param {String} event event name. + * @param {...*} args arguments passed to each listener. + */ + emit(event, ...args) { + if (!events[event]) return + // call every subscribed listener + events[event].forEach((fn) => fn(...args)) + }, + + /** + * Subscribes a listener that auto-unsubscribes after its first emit. + * @param {String} event event name. + * @param {Function} listener callback function. + */ + once(event, listener) { + // wrap listener in a self-removing function + const wrapper = (...args) => { + listener(...args) + this.off(event, wrapper) // remove wrapper (not original listener) after firing + } + this.on(event, wrapper) + }, + } +} + +console.log("\n--- X3: createEmitter ---") +const emitter = createEmitter() + +const greet = (name) => console.log(`Hello, ${name}!`) +emitter.on("say", greet) +emitter.emit("say", "Yoandy") // Hello, Yoandy! +emitter.emit("say", "Kevin") // Hello, Kevin! +emitter.off("say", greet) +emitter.emit("say", "Alice") // (nothing — unsubscribed) + +const onceHi = (name) => console.log(`Hi once, ${name}!`) +emitter.once("greet", onceHi) +emitter.emit("greet", "Bob") // Hi once, Bob! +emitter.emit("greet", "Dan") // (nothing — auto-unsubscribed) + +// Bonus: multiple listeners for the same event +const emitter2 = createEmitter() +emitter2.on("data", (x) => console.log(" listener A:", x)) +emitter2.on("data", (x) => console.log(" listener B:", x * 2)) +emitter2.emit("data", 5) +// listener A: 5 +// listener B: 10 + +// ════════════════════════════════════════ +// CLOSURE CHEATSHEET — reference card +// ════════════════════════════════════════ +/* + Every advanced function pattern = closure skeleton + different memory + + ┌──────────┬──────────────────────────┬────────────────────────────────┐ + │ Pattern │ What it closes over │ What it does with memory │ + ├──────────┼──────────────────────────┼────────────────────────────────┤ + │ memoize │ Map of arg → result │ Read before compute, write after│ + │ curry │ Args collected so far │ Accumulate until fn.length met │ + │ once │ called flag + result │ Guard the call, cache result │ + │ debounce │ Timer ID │ Cancel old timer, set new one │ + │ pipe │ Composed fn chain │ Thread output → input (yours ✓) │ + └──────────┴──────────────────────────┴────────────────────────────────┘ + + The skeleton (memorise this): + + const pattern = (config) => { + let memory = ... // ← declared once, lives forever + return (...args) => { // ← this function closes over memory + // read/update memory + // do the work + } + } + + Your pipe() was already this. You wrote it perfectly. + memoize, curry, once, debounce are the same idea. +*/ diff --git a/fullstack/javascript/4-array/exam/functions-arrays-exam.js b/fullstack/javascript/4-array/exam/functions-arrays-exam.js new file mode 100644 index 0000000..59aeda6 --- /dev/null +++ b/fullstack/javascript/4-array/exam/functions-arrays-exam.js @@ -0,0 +1,1043 @@ +/** + * ======================================== + * FUNCTIONS & ARRAYS MASTERY EXAM + * ======================================== + * Student: Yoandy Doble Herrera + * Date: 01/05/2026 + * Start Time: 08:43 am elapsed time 35 hours + * End Time: 14:03 pm + * + * RULES: + * - No external libraries. Vanilla JS only. + * - No copy-pasting from previous files. + * - console.log your results — every function must be tested. + * - Write JSDoc for every function. + * - Read each problem fully before coding. + * + * STRUCTURE: + * Part 1 — Theory & Predictions (25 pts) + * Part 2 — Array Methods (30 pts) + * Part 3 — Functions deep dive (25 pts) + * Part 4 — Combined challenges (20 pts) + * ───────────────────────────────────────── + * Total: 100 pts | Passing: 75 pts + * ======================================== + */ + +// ════════════════════════════════════════ +// PART 1 — THEORY & PREDICTIONS (25 pts) +// ════════════════════════════════════════ +// For each block: write what you expect the output to be AS A COMMENT +// before running the code. Then run it and check. +// Award yourself full points only if your prediction was correct BEFORE running. + +console.log("\n=== Part 1 — Theory & Predictions ===") + +// ── Q1 (3 pts) ─────────────────────────── +// Predict the output of each line. Write your answer as a comment next to each. +const arr1 = [1, 2, 3, 4, 5] +console.log(arr1.length) // Your prediction: 5 +console.log(arr1[arr1.length - 1]) // Your prediction: 5 +console.log(arr1[-1]) // Your prediction: undefined + +// ── Q2 (3 pts) ─────────────────────────── +// Predict what gets logged. +const a = [1, 2, 3] +const b = a +b.push(4) +console.log(a) // Your prediction: [1,2,3,4] +console.log(a === b) // Your prediction: true + +const c = [1, 2, 3] +const d = [...c] +d.push(4) +console.log(c) // Your prediction: [1,2,3] + +// ── Q3 (3 pts) ─────────────────────────── +// Predict the output. +function outer(x) { + function inner(y) { + return x + y + } + return inner +} +const add5 = outer(5) +console.log(add5(3)) // Your prediction: 8 +console.log(add5(10)) // Your prediction: 15 +console.log(outer(2)(8)) // Your prediction: 10 + +// ── Q4 (3 pts) ─────────────────────────── +// Predict the output. +const nums = [1, 2, 3, 4, 5] +const result1 = nums.map((n) => n * 2).filter((n) => n > 4) +console.log(result1) // Your prediction: [6,8,10] + +const result2 = nums.reduce((acc, n) => acc + n, 10) +console.log(result2) // Your prediction: 25 + +// ── Q5 (4 pts) ─────────────────────────── +// Predict the output. Think carefully about scope and hoisting. +console.log(typeof myFunc) // Your prediction: function +//console.log(typeof myArrow) // Your prediction: ReferenceError: Cannot access 'myArrow' before initialization + +function myFunc() { + return 42 +} +const myArrow = () => 42 + +console.log(myFunc()) // Your prediction: 42 +console.log(myArrow()) // Your prediction: 42 + +// ── Q6 (4 pts) ─────────────────────────── +// Predict the output. Think about mutation vs. copying. +const matrix = [ + [1, 2], + [3, 4], +] +const flat1 = matrix.flat() +console.log(flat1) // Your prediction: [1,2,3,4] + +const words = ["hello world", "foo bar"] +const flat2 = words.flatMap((w) => w.split(" ")) +console.log(flat2) // Your prediction: [ 'hello', 'world', 'foo', 'bar' ] + +// ── Q7 (5 pts) ─────────────────────────── +// What does this function do? Write a one-sentence explanation as a comment, +// then predict the three outputs. + +/** + * + * @param {Array} arr + * @param {Function} fn + * @returns + */ +const mystery = (arr, fn) => + arr.reduce((acc, val) => { + const key = fn(val) + if (!acc[key]) acc[key] = [] + acc[key].push(val) + return acc + }, {}) + +const people = ["Alice", "Bob", "Anna", "Brian", "Charlie"] +const grouped = mystery(people, (name) => name[0]) +console.log(grouped) // Your prediction: { A: [ 'Alice', 'Anna' ], B: [ 'Bob', 'Brian' ], C: [ 'Charlie' ] } +console.log(grouped["A"]) // Your prediction: [ 'Alice', 'Anna' ] +console.log(grouped["B"].length) // Your prediction: 2 + +// One-sentence explanation of what mystery() does: Function that receives an arry as first parameter and a function as second parameter. The function reduces array accumulated result into object with first letter word of each non repeated letter as key. Group words by key in array into arrays ex. A: [ 'Alice', 'Anna' ]. Return an object. +// YOUR ANSWER: Return an object with all ocurrences first letter grouped. + +// ════════════════════════════════════════ +// PART 2 — ARRAY METHODS (30 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 2 — Array Methods ===") + +// ── C1 (4 pts) ─────────────────────────── +/** + * Given an array of integers, return a new array containing only the unique values, + * preserving the order of first appearance. + * Do NOT use Set directly on the array — implement the logic yourself using array methods. + * + * unique([1, 2, 1, 3, 2, 4]) → [1, 2, 3, 4] + * unique([5, 5, 5]) → [5] + * unique([]) → [] + */ +// Your solution: +/** + * Function that return a new array containing only the unique values, + * preserving the order of first appearance. + * @param {Array} arr an array of integers. + * @returns {Array} a new array containing only the unique values. + */ +const unique = (arr) => { + return arr.reduce((acc, item) => { + if (!acc.includes(item)) acc.push(item) + return acc + }, []) +} + +// --- Test Cases --- +console.log("--- Challenge 1 Running Tests ---") +console.log(unique([1, 2, 1, 3, 2, 4])) // expected: [ 1, 2, 3, 4 ] +console.log(unique([5, 5, 5])) // expected: [ 5 ] +console.log(unique([])) // expected: [] +console.log(unique([10, 20, 30, 10, 20])) // expected: [10, 20, 30] +console.log(unique([0, 0, 1, 1, 2])) // expected: [0, 1, 2] +console.log(unique([-1, -1, 0, 1, 1])) // expected: [-1, 0, 1] +console.log(unique([3, 1, 4, 1, 5, 9, 2, 6, 5])) // expected: [3, 1, 4, 5, 9, 2, 6] +console.log("\n====================================") + +// ── C2 (4 pts) ─────────────────────────── +/** + * Given an array of strings, return an object where each key is a string + * and the value is the number of times it appears in the array. + * Use reduce. + * + * countOccurrences(["a","b","a","c","b","a"]) → { a: 3, b: 2, c: 1 } + * countOccurrences([]) → {} + */ +// Your solution: +/** + * Function that count string occurrences. + * @param {Array} arr any array of strings. + * @returns {Object} return an object where each key is a string + * and the value is the number of times it appears in the array. + */ +const countOccurrences = (arr) => { + return arr.reduce((acc, val) => { + acc[val] = (acc[val] || 0) + 1 + return acc + }, {}) +} + +// --- Test Cases --- +console.log("--- Challenge 2 Running Tests ---") +console.log(countOccurrences(["a", "b", "a", "c", "b", "a"])) // { a: 3, b: 2, c: 1 } +console.log(countOccurrences([])) // expected: {} +console.log(countOccurrences(["x", "x", "y"])) // { x: 2, y: 1 } +console.log(countOccurrences([1, 2, 1, 3, 2, 1])) // { '1': 3, '2': 2, '3': 1 } +console.log(countOccurrences(["cat", "dog", "cat"])) // { cat: 2, dog: 1 } +console.log(countOccurrences([10, 10, 10, 20])) // { '10': 3, '20': 1 } +console.log(countOccurrences(["apple", "banana", "apple", "cherry", "banana", "apple"])) // { apple: 3, banana: 2, cherry: 1 } +console.log(countOccurrences(["A", "a", "A", "a"])) // { A: 2, a: 2 } +console.log("\n====================================") + +// ── C3 (5 pts) ─────────────────────────── +/** + * Given a nested array of any depth, return a fully flattened array. + * Do NOT use Array.flat() or Array.flatMap(). + * Implement it yourself — think recursion. + * + * deepFlat([1, [2, [3, [4]], 5]]) → [1, 2, 3, 4, 5] + * deepFlat([1, 2, 3]) → [1, 2, 3] + * deepFlat([]) → [] + */ +// Your solution: +/** + * Function that implement Array.flat() or Array.flatMap() given a nested array of any depth, return a fully flattened array. + * @param {Array} arr a nested array of any depth. + * @returns {Array} return a fully flattened array. + */ +const deepFlat = (arr) => { + return arr.reduce((acc, num) => { + if (typeof num === "number") { + // its a number + acc.push(num) + } else { + // Array, apply recursion + acc.push(...deepFlat(num)) + } + return acc + }, []) +} + +// --- Test Cases --- +console.log("--- Challenge 3 Running Tests ---") +console.log(deepFlat([1, [2, [3, [4]], 5]])) // expected: [1, 2, 3, 4, 5] +console.log(deepFlat([1, 2, 3])) // expected: [1, 2, 3] +console.log(deepFlat([])) // expected: [] +console.log("\n====================================") + +// ── C4 (4 pts) ─────────────────────────── +/** + * Given an array of objects representing students, return a new array + * containing only students who passed (score >= 60), sorted by score descending, with only their name and score (no other properties). + * + * Input: + * [ + * { name: "Alice", score: 90, age: 20 }, + * { name: "Bob", score: 45, age: 22 }, + * { name: "Carol", score: 75, age: 19 }, + * { name: "Dave", score: 60, age: 21 }, + * ] + * Output: + * [ + * { name: "Alice", score: 90 }, + * { name: "Carol", score: 75 }, + * { name: "Dave", score: 60 }, + * ] + */ +const students = [ + { name: "Alice", score: 90, age: 20 }, + { name: "Bob", score: 45, age: 22 }, + { name: "Carol", score: 75, age: 19 }, + { name: "Dave", score: 60, age: 21 }, +] +const studentsPhi = [ + { name: "Alexa", score: 45, age: 22 }, + { name: "Carol", score: 75, age: 30 }, + { name: "Javier", score: 59, age: 21 }, + { name: "Karen", score: 80, age: 25 }, +] +const studentsUCI = [ + { name: "Bob", score: 45, age: 22 }, + { name: "Briyi", score: 75, age: 19 }, + { name: "Kevin", score: 59, age: 25 }, + { name: "Jonathan", score: 84, age: 20 }, + { name: "Melissa", score: 75, age: 28 }, + { name: "Manuel", score: 75, age: 19 }, + { name: "Julio Cesar", score: 77, age: 36 }, + { name: "Yoandy", score: 95, age: 37 }, +] + +const studentsUH = [ + { name: "Cristina", score: 85, age: 22 }, + { name: "Briyi", score: 75, age: 19 }, + { name: "Barbara", score: 93, age: 28 }, + { name: "Jonathan", score: 56, age: 20 }, + { name: "Melissa", score: 75, age: 28 }, +] +// Your solution: +/** + * Function that given an array of objects representing students return a new array containing only students who passed (score >= 60), sorted by score descending, with only their name and score. + * @param {Array} arr an array of objects representing students. + * @returns {Array} a new array containing only students who passed (score >= 60), sorted by score descending, with only their name and score. + */ +const graduates = (arr) => { + return arr + .filter((student) => { + return student.score >= 60 + }) + .map((student) => { + return { name: student.name, score: student.score } + }) + .sort((a, b) => b.score - a.score) +} + +// --- Test Cases --- +console.log("--- Challenge 4 Running Tests ---") +console.log(graduates(students)) // expected: [{ name: "Alice", score: 90 }, { name: "Carol", score: 75 }, { name: "Dave", score: 60 }, ] +console.log(graduates(studentsPhi)) // expected: [{ name: "Karen", score: 80, age: 25 }, { name: "Carol", score: 75, age: 30 },] +console.log(graduates(studentsUCI)) // expected: [{ name: "Yoandy", score: 95, age: 37 }, { name: "Jonathan", score: 84, age: 20 }, { name: "Julio Cesar", score: 77, age: 36 }, { name: "Briyi", score: 75, age: 19 }, { name: "Melissa", score: 75, age: 28 }, { name: "Manuel", score: 75, age: 19 },] +console.log(graduates(studentsUH)) // expected: [{ name: 'Barbara', score: 93 }, { name: 'Cristina', score: 85 }, { name: 'Briyi', score: 75 }, { name: 'Melissa', score: 75 }] +console.log("\n====================================") + +// ── C5 (5 pts) ─────────────────────────── +/** + * Implement your own version of Array.prototype.map WITHOUT using + * map, filter, reduce, or forEach. Use a for loop or for...of. + * It must behave exactly like the native map. + * + * myMap([1, 2, 3], x => x * 2) → [2, 4, 6] + * myMap(["a", "b"], s => s.toUpperCase()) → ["A", "B"] + * myMap([], x => x) → [] + */ +// Your solution: +/** + * Function that implements own version of Array.prototype.map WITHOUT using + * map, filter, reduce, or forEach. + * @param {Array} arr any array. + * @param {Function} fnc function to map array. + * @returns {Array} mapped array. + */ +const myMap = (arr, fnc) => { + if (arr.length === 0) return [] + const result = [] + for (const element of arr) { + const mappedElement = fnc(element) + result.push(mappedElement) + } + return result +} + +// --- Test Cases --- +console.log("--- Challenge 5 Running Tests ---") +console.log(myMap([1, 2, 3], (x) => x * 2)) // expected: [2, 4, 6] +console.log(myMap(["a", "b"], (s) => s.toUpperCase())) // expected: ["A", "B"] +console.log(myMap(["bob", "briyi", "Kevin"], (student) => student[0].toUpperCase() + student.slice(1))) // expected: ["Bob", "Briyi","Kevin"] +console.log(myMap([], (x) => x)) // expected: [] +console.log("\n====================================") + +// ── C6 (4 pts) ─────────────────────────── +/** + * Given an array of numbers, return an object with: + * - min: the minimum value + * - max: the maximum value + * - avg: the average (rounded to 2 decimal places) + * - sum: the total sum + * Use a SINGLE reduce call — no multiple passes over the array. + * + * stats([3, 1, 4, 1, 5, 9]) → { min: 1, max: 9, avg: 3.83, sum: 23 } + * stats([10]) → { min: 10, max: 10, avg: 10, sum: 10 } + */ +// Your solution: + +/** + * Function that given an array of numbers gets min, max, avg and sum. + * @param {Array} arr any array of numbers. + * @returns {Object} return min, max, avg and sum from array of numbers. + */ +const stats = (arr) => { + if (arr.length === 0) return {} + //let result = {} + // 1. Get min array value + const minimun = Math.min(...arr) + // 2. Get max array value + const maximun = Math.max(...arr) + let result = arr.reduce((acc, item) => { + //console.log(">>>Acumalate previous", acc) + //console.log(">>>Actual", item) + // 3. Add min to obj. + if (!Object.hasOwn(acc, "min")) acc.min = minimun + // 4. Add max to obj. + if (!Object.hasOwn(acc, "max")) acc.max = maximun + // 5. Avg + acc["avg"] = (acc["avg"] || 0) + item + // 6. Sum + acc["sum"] = (acc["sum"] || 0) + item + /* Obj Updated */ + return acc + }, {}) + result.avg = Math.floor(result.avg / arr.length) + return result +} + +// --- Test Cases --- +console.log("--- Challenge 6 Running Tests ---") +console.log(stats([3, 1, 4, 1, 5, 9])) // expected: { min: 1, max: 9, avg: 3.83, sum: 23 } +console.log(stats([10])) // expected: { min: 10, max: 10, avg: 10, sum: 10 } +console.log(stats([12, -5, 33, 10])) // expected: { min: -5, max: 33, avg: 12, sum: 50 } +console.log(stats([3, 1, 4, 1, 5, 9, 2, 6, 5])) // expected: { min: 1, max: 9, avg: 4, sum: 36 } +console.log(stats([10, 20, 10, 30, 20, 10])) // expected: { min: 10, max: 30, avg: 16, sum: 100 } +console.log("\n====================================") + +// ── C7 (4 pts) ─────────────────────────── +/** + * Given two arrays, return true if they have the same elements + * regardless of order (treat as multisets — duplicates matter). + * + * sameElements([1,2,3], [3,2,1]) → true + * sameElements([1,2,2], [2,1,2]) → true + * sameElements([1,2,3], [1,2,4]) → false + * sameElements([1,2], [1,2,3]) → false + * + * Hint: think about sorting, or counting occurrences. + */ +// Your solution: + +/** + * Function that checks if given two arrays returns true/false if they have the same elements. + * @param {Array} arr1 any number array. + * @param {Array} arr2 any number array. + * @returns {Boolean} return true if they have the same elements + * regardless of order + */ +const sameElements = (arrOne, arrTwo) => { + // 1. Check length + if (arrOne.length !== arrTwo.length) return false + + // 2. Create map occurrences + const arrOneMap = arrToMap(arrOne) + const arrTwoMap = arrToMap(arrTwo) + + // 3. Compare maps + let decision = true + for (const [key, value] of arrOneMap.entries()) { + if (!arrTwoMap.has(key) || arrTwoMap.get(key) !== value) { + decision = false + break + } + } + return decision +} + +/** + * Function that count element occurrences and create and Map. + * @param {Array} arr any number array. + * @returns {Map} ocurrences Map. + */ +const arrToMap = (arr) => { + let arrMap = new Map() + for (const item of arr) { + arrMap.set(item, (arrMap.get(item) || 0) + 1) + } + return arrMap +} + +// --- Test Cases --- +console.log("--- Challenge 7 Running Tests ---") +console.log(sameElements([1, 2, 3], [3, 2, 1])) // expected: true +console.log(sameElements([1, 2, 2], [2, 1, 2])) // expected: true +console.log(sameElements([1, 2, 3], [1, 2, 4])) // expected: false +console.log(sameElements([1, 2], [1, 2, 3])) // expected: false +console.log("\n====================================") + +// ════════════════════════════════════════ +// PART 3 — FUNCTIONS DEEP DIVE (25 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 3 — Functions Deep Dive ===") + +// ── F1 (4 pts) ─────────────────────────── +/** + * Write a function memoize(fn) that takes any function and returns a new function that caches its results. If called again with the same argument, return the cached result without calling fn again. + * const slowDouble = n => { ...some slow computation... return n * 2 } + * const fastDouble = memoize(slowDouble) + * fastDouble(5) → 10 (computed) + * fastDouble(5) → 10 (from cache — fn not called again) + * fastDouble(3) → 6 (computed) + */ +// Your solution: + +/** + * Function memoize(fn) that takes any function and returns a new function that caches its results. + * @param {Function} fn any function. + * @returns returns a new function that caches its results. + */ +const memoize = (fn) => { + return (x) => fn(x) +} + +/** + * Function that given a number pow by 2. + * @param {Number} n any number. + * @returns {Number} numbur duplicated. + */ +const slowDouble = (n) => { + return n * 2 +} +const fastDouble = memoize(slowDouble) +// --- Test Cases --- +console.log("--- Challenge 1 Running Tests ---") +console.log(fastDouble(5)) // expected: 10 (computed) +console.log(fastDouble(5)) // expected: 10 (from cache — fn not called again) +console.log(fastDouble(3)) // expected: 6 (computed) +console.log("\n====================================") + +// ── F2 (4 pts) ─────────────────────────── +/** + * Write a function pipe(...fns) that takes any number of functions and returns a new function that applies them left to right. + * The output of each function becomes the input of the next. + * + * const process = pipe( + * x => x * 2, + * x => x + 1, + * x => x ** 2 + * ) + * process(3) → ((3 * 2) + 1) ** 2 → 49 + * process(0) → ((0 * 2) + 1) ** 2 → 1 + */ +// Your solution: + +/** + * Function that takes any number of functions and returns a new function that applies them left to right. + * @param {...Function} fns array of functions. + * @returns a new function that applies them left to right. + */ +const pipe = (...fns) => { + return fns.reduce( + (fn, nextFn) => { + return (x) => nextFn(fn(x)) + }, + (x) => x, + ) +} + +// --- Test Cases --- +console.log("--- Challenge 2 Running Tests ---") +const process = pipe( + (x) => x * 2, + (x) => x + 1, + (x) => x ** 2, +) +console.log(process(3)) // expected: ((3 * 2) + 1) ** 2 → 49 +console.log(process(0)) // expected: ((0 * 2) + 1) ** 2 → 1 +console.log(process(15)) // expected: ((15 * 2) + 1) ** 2 → 961 +console.log("\n====================================") + +// ── F3 (5 pts) ─────────────────────────── +/** + * Write a function curry(fn) that transforms a function of N arguments + * into a chain of N single-argument functions. + * + * const add = (a, b, c) => a + b + c + * const curriedAdd = curry(add) + * curriedAdd(1)(2)(3) → 6 + * curriedAdd(10)(20)(30) → 60 + * + * Hint: check fn.length to know how many arguments are expected. + * Think recursion — if not enough args collected yet, return another function. + */ +// Your solution: + +/** + * Function that transforms a function of N arguments + * into a chain of N single-argument functions. + * @param {Function} fn + */ +const curry = (fn) => { + return (...args) => fn(...args) +} + +// --- Test Cases --- +console.log("--- Challenge 3 Running Tests ---") +const add = (a, b, c) => a + b + c +const curriedAdd = curry(add) +//console.log(curriedAdd(1)(2)(3)) // expected: 6 +//console.log(curriedAdd(10)(20)(30)) // expected: 60 +console.log("\n====================================") + +// ── F4 (4 pts) ─────────────────────────── +/** + * Write a function once(fn) that ensures fn is only called ONCE. + * Every subsequent call returns the result of the first call + * without executing fn again. + * + * let count = 0 + * const increment = once(() => ++count) + * increment() → 1 + * increment() → 1 (not 2 — fn not called again) + * increment() → 1 + * console.log(count) → 1 + */ +// Your solution: + +/** + * Function that ensures fn is only called ONCE. + * @param {Function} fn any function. + * @returns {Function} result of the first call without executing fn again. + */ +const once = (fn) => { + let iterator = 0 + return () => { + if (++iterator === 1) fn() + } +} + +// --- Test Cases --- +console.log("--- Challenge 4 Running Tests ---") +let count = 0 +const increment = once(() => ++count) +increment() //→ 1 +increment() //→ 1 (not 2 — fn not called again) +console.log(count) +increment() //→ 1 +console.log(count) //→ 1 +console.log("\n====================================") + +// ── F5 (4 pts) ─────────────────────────── +/** + * Write a recursive function flatten that works like deepFlat from C3 + * but implemented as a pure recursive function using no loops — + * only recursion and array spread/concat. + * + * flattenRecursive([1, [2, [3]]]) → [1, 2, 3] + * + * Then: write the same function using reduce instead of recursion. + * flattenReduce([1, [2, [3]]]) → [1, 2, 3] + */ + +// Your solution (recursive with spread): +/** + * Function that implement Array.flat() or Array.flatMap() given a nested array of any depth, return a fully flattened array. + * @param {Array} arr a nested array of any depth. + * @returns {Array} return a fully flattened array. + */ +const deepFlatWithSpread = (arr) => { + if (arr.length === 0) return [] + const [first, ...rest] = arr + if (typeof first === "number") { + return [first, ...deepFlatWithSpread(rest)] + } else { + return deepFlatWithSpread(first.concat(rest)) + } +} + +// Your solution (with reduce): + +/** + * Function that implement Array.flat() or Array.flatMap() given a nested array of any depth, return a fully flattened array. + * @param {Array} arr a nested array of any depth. + * @returns {Array} return a fully flattened array. + */ +const deepFlatWithReduce = (arr) => { + return arr.reduce((acc, num) => { + if (typeof num === "number") { + // its a number + acc.push(num) + } else { + // Array, apply recursion + acc.push(...deepFlatWithReduce(num)) + } + return acc + }, []) +} + +// --- Test Cases --- +console.log("--- Challenge 5 Running Tests ---") +console.log(">>>deepFlatWithSpread", deepFlatWithSpread([1, [2, [35, 58, [36]]], [2, [3, [4]], 5]])) // expected: [1, 2, 3, 4, 5] +console.log(">>>deepFlatWithSpread", deepFlatWithSpread([1, 2, 3])) // expected: [1, 2, 3] + +console.log(">>>deepFlatWithSpread", deepFlatWithSpread([])) // expected: [] +console.log(">>>deepFlatWithReduce", deepFlatWithReduce([1, [2, [35, 58, [36]]], [2, [3, [4]], 5]])) // expected: [1, 2, 3, 4, 5] +console.log(">>>deepFlatWithReduce", deepFlatWithReduce([1, 2, 3])) // expected: [1, 2, 3] +console.log(">>>deepFlatWithReduce", deepFlatWithReduce([])) // expected: [] +console.log("\n====================================") + +// ── F6 (4 pts) ─────────────────────────── +/** + * Write a function debounce(fn, delay) that delays invoking fn until + * after `delay` milliseconds have elapsed since the last time it was called. + * If called again before the delay expires, reset the timer. + * + * This is a real-world pattern used in search inputs, resize handlers, etc. + * + * const log = debounce(msg => console.log(msg), 300) + * log("a") // timer starts + * log("b") // timer resets + * log("c") // timer resets + * // after 300ms of silence → "c" is logged (only once) + * + * Hint: use setTimeout and clearTimeout. + * Test it with setTimeout calls at different intervals. + */ +// Your solution: + +/** + * Function that delays invoking fn until + * after `delay` milliseconds have elapsed since the last time it was called. + * If called again before the delay expires, reset the timer. + * @param {Function} fn + * @param {Number} delay + */ +const debounce = (fn, delay) => { + let count = 0 + let timeID = undefined + return (msg) => { + if (count === 0) { + timeID = setTimeout(fn(msg), delay) + console.log("TimeID", timeID) + count++ + } else { + if (timeID !== undefined) { + clearTimeout(timeID) + } + } + } +} + +// --- Test Cases --- +console.log("--- Challenge 6 Running Tests ---") + +console.log("\n====================================") +// ════════════════════════════════════════ +// PART 4 — COMBINED CHALLENGES (20 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 4 — Combined Challenges ===") + +// ── X1 (6 pts) ─────────────────────────── +/** + * Implement a pipeline function that processes a dataset of products. + * + * Given this array of products: + */ +const products = [ + { name: "Laptop", price: 1200, category: "tech", stock: 5 }, + { name: "Phone", price: 800, category: "tech", stock: 0 }, + { name: "Desk", price: 350, category: "furniture", stock: 3 }, + { name: "Monitor", price: 600, category: "tech", stock: 2 }, + { name: "Chair", price: 250, category: "furniture", stock: 7 }, + { name: "Tablet", price: 400, category: "tech", stock: 0 }, + { name: "Keyboard", price: 150, category: "tech", stock: 10 }, +] +/** + * Using ONLY array methods (no for loops), return: + * { + * available: [...], // products with stock > 0, sorted by price descending + * byCategory: { tech: [...], furniture: [...] }, // grouped by category (names only) + * totalValue: number, // sum of (price * stock) for available products only + * cheapest: object, // the cheapest available product (full object) + * } + */ +// Your solution: + +/** + * Function that processes a dataset of products. + * @param {Array} arr any array of product objects. + * @returns {Object} processed dataset of products. + */ +const pipeline = (arr) => { + let result = {} + // 1. Available products + const available = availableProduct(arr) + //console.log(">>>Available", available) + result["available"] = available + + //2. Product category + const categoryProduct = category(arr) + //console.log(">>>byCategory", categoryProduct) + result["byCategory"] = categoryProduct + + // 3. totalValue + const totalValue = total(available) + //console.log(">>>Total Available", totalValue) + result["totalValue"] = totalValue + + // 4. Cheapest available product + const cheapest = available[available.length - 1] + //console.log(">>>Cheapest Available", cheapest) + result["cheapest"] = cheapest + return result +} + +/** + * Function that given a dataset of products returns products with stock > 0, sorted by price descending. + * @param {Array} arr any array of product objects + * @returns {Object} returns products with stock > 0, sorted by price descending. + */ +const availableProduct = (arr) => { + return arr + .filter((product) => { + return product.stock > 0 + }) + .sort((a, b) => b.price - a.price) +} + +/** + * + * @param {Array} arr + * @returns {Object} + */ +const category = (arr) => { + return arr.reduce((acc, product) => { + if (!Object.hasOwn(acc, product.category)) { + acc[product.category] = [] + acc[product.category].push(product.name) + } else { + acc[product.category].push(product.name) + } + return acc + }, {}) +} + +/** + * Function that given available products returns sum of (price * stock) only. + * @param {Array} arr any array of available products. + * @returns {Number} sum of (price * stock) for available products. + */ +const total = (arr) => { + return arr.reduce((acc, product) => { + return acc + product.price * product.stock + }, 0) +} + +// --- Test Cases --- +console.log("--- Challenge 1 Running Tests ---") +console.log(pipeline(products)) // expected: +console.log("\n====================================") + +// ── X2 (7 pts) ─────────────────────────── +/** + * Write a function makeValidator(rules) that: + * - Takes an object of validation rules (each rule is a function that returns true/false) + * - Returns a validate(data) function + * - validate(data) returns { valid: boolean, errors: string[] } + * + * Example: + * const validator = makeValidator({ + * name: value => value.length >= 2, + * age: value => value >= 18 && value <= 120, + * email: value => value.includes("@"), + * }) + * + * validator({ name: "Y", age: 25, email: "y@x.com" }) + * → { valid: false, errors: ["name"] } + * + * validator({ name: "Yoandy", age: 25, email: "y@x.com" }) + * → { valid: true, errors: [] } + * + * validator({ name: "Y", age: 15, email: "notanemail" }) + * → { valid: false, errors: ["name", "age", "email"] } + */ +// Your solution: + +/** + * Function that takes an object of validation rules (each rule is a function that returns true/false) + * - Returns a validate(data) function + * - validate(data) returns { valid: boolean, errors: string[] } + * + * @param {Object} obj any object of validation rules (each rule is a function that returns true/false) + * @returns {Object} returns a validate(data) function - validate(data) returns { valid: boolean, errors: string[] } + */ +const makeValidator = (obj) => { + return (data) => { + let isValid = { valid: true, errors: [] } + // 1. loop obj + for (const [key, value] of Object.entries(obj)) { + // 2. checks key in obj data to test + if (key in data) { + // 3. apply fn to data true/false + if (!value(data[key])) { + isValid.valid = false + isValid.errors.push(key) + } + } else { + isValid.valid = false + isValid.errors.push(key) + } + } + return isValid + } +} + +const validator = makeValidator({ + name: (value) => value.length >= 2, + age: (value) => value >= 18 && value <= 120, + email: (value) => value.includes("@"), +}) + +// --- Test Cases --- +console.log("--- Challenge 2 Running Tests ---") +console.log(validator({ name: "Y", age: 25, email: "y@x.com" })) +console.log(validator({ name: "Yoandy", age: 25, email: "y@x.com" })) +console.log(validator({ name: "Y", age: 15, email: "notanemail" })) +console.log("\n====================================") +// ── X3 (7 pts) ─────────────────────────── +/** + * Implement a simple observable/event emitter from scratch. + * It must support: + * - on(event, listener) — subscribe a function to an event + * - off(event, listener) — unsubscribe a specific listener + * - emit(event, ...args) — call all listeners for that event with given args + * - once(event, listener) — subscribe but auto-unsubscribe after first emit + * + * Example: + * const emitter = createEmitter() + * + * const greet = name => console.log(`Hello, ${name}!`) + * emitter.on("say", greet) + * emitter.emit("say", "Yoandy") → "Hello, Yoandy!" + * emitter.emit("say", "Kevin") → "Hello, Kevin!" + * emitter.off("say", greet) + * emitter.emit("say", "Alice") → (nothing — unsubscribed) + * + * const onceHi = name => console.log(`Hi once, ${name}!`) + * emitter.once("greet", onceHi) + * emitter.emit("greet", "Bob") → "Hi once, Bob!" + * emitter.emit("greet", "Dan") → (nothing — auto-unsubscribed) + */ +// Your solution: + +/** + * A simple observable/event emitter from scratch. + * It supports: + * - on(event, listener) — subscribe a function to an event + * - off(event, listener) — unsubscribe a specific listener + * - emit(event, ...args) — call all listeners for that event with given args + * - once(event, listener) — subscribe but auto-unsubscribe after first emit + * + * Example: + * const emitter = createEmitter() + * + * const greet = name => console.log(`Hello, ${name}!`) + * emitter.on("say", greet) + * emitter.emit("say", "Yoandy") → "Hello, Yoandy!" + * emitter.emit("say", "Kevin") → "Hello, Kevin!" + * emitter.off("say", greet) + * emitter.emit("say", "Alice") → (nothing — unsubscribed) + * + * const onceHi = name => console.log(`Hi once, ${name}!`) + * emitter.once("greet", onceHi) + * emitter.emit("greet", "Bob") → "Hi once, Bob!" + * emitter.emit("greet", "Dan") → (nothing — auto-unsubscribed) + * + */ +const createEmitter = () => { + return { + suscribed: {}, + /** + * Subscribe a function to an event -void-. + * @param {String} event any event. + * @param {Function} listener any function. + * @returns + */ + on(event, listener) { + if (!Object.hasOwn(this.suscribed, event)) { + this.suscribed[event] = [listener, false, false] + } + }, + /** + * Unsubscribe a specific listener -void-. + * @param {String} event any suscribed event. + * @param {Function} listener any specific listener event. + */ + off(event, listener) { + if (Object.hasOwn(this.suscribed, event) && this.suscribed[event][0] === listener) { + delete this.suscribed[event] + } else { + console.error(">>>-OFF-Event not suscribed", event) + } + }, + /** + * Call all listeners for that event with given args -void-. + * @param {String} event any suscribed event. + * @param {...Array} args + */ + emit(event, ...args) { + // 1. Checks event in obj suscribed + if (Object.hasOwn(this.suscribed, event)) { + // 2. Checks fn once active + if (!this.suscribed[event][2]) { + // 3. Perform each listener on args + args.forEach((listener) => { + this.suscribed[event][0](listener) + }) + // 4. Update emitted event + this.suscribed[event][1] = true + } else { + // 5. fn once active + if (this.suscribed[event][1] === false) { + // 6. Emit first time with once actived + // 7. Perform each listener on args + args.forEach((listener) => { + this.suscribed[event][0](listener) + }) + // 8. Update emitted event + this.suscribed[event][1] = true + } + } + } + }, + /** + * Subscribe but auto-unsubscribe after first emit -void-. + * @param {*} event + * @param {*} listener + */ + once(event, listener) { + // 1. Suscribe + this.on(event, listener) + // 2. Active status once in suscribe obj + this.suscribed[event][2] = true + }, + } +} + +// --- Test Cases --- +console.log("--- Challenge 3 Running Tests ---") +const emitter = createEmitter() +const greet = (name) => console.log(`Hello, ${name}!`) +emitter.on("say", greet) +emitter.emit("say", "Yoandy", "Laura") //→ "Hello, Yoandy!" → "Hello, Laura!" +emitter.emit("say", "Kevin") //→ "Hello, Kevin!" +emitter.off("say", greet) +emitter.emit("say", "Alice") //→ (nothing — unsubscribed) + +const onceHi = (name) => console.log(`Hi once, ${name}!`) +emitter.once("greet", onceHi) +emitter.emit("greet", "Bob") //→ "Hi once, Bob!" +emitter.emit("greet", "Dan") //→ (nothing — auto-unsubscribed) +console.log("\n==================END OF EXAM==================") + +// ════════════════════════════════════════ +// END OF EXAM +// ════════════════════════════════════════ +/* + Before submitting, verify: + [ ] Every function has a JSDoc comment + [ ] Every function has at least 2 console.log tests + [ ] No syntax errors (run: node this-file.js) + [ ] Your name and end time are filled in at the top +*/ diff --git a/fullstack/javascript/4-array/exam/gemini-code-solution-exam.js b/fullstack/javascript/4-array/exam/gemini-code-solution-exam.js new file mode 100644 index 0000000..364df88 --- /dev/null +++ b/fullstack/javascript/4-array/exam/gemini-code-solution-exam.js @@ -0,0 +1,102 @@ +/** + * ======================================== + * PART 3 & 4 FIXES: CLOSURE PATTERNS + * ======================================== + */ + +// ── F1: Memoize ─────────────────────────── +// Bug in original: You just returned the function call without saving the result. +const memoize = (fn) => { + const cache = new Map(); // <-- The Closure "Backpack" + + return (...args) => { + // Convert arguments to a string so we can use it as a key + const key = JSON.stringify(args); + + if (cache.has(key)) { + return cache.get(key); // Return cached result + } + + const result = fn(...args); + cache.set(key, result); // Save to backpack + return result; + } +} + +// ── F3: Curry ───────────────────────────── +// Bug in original: You returned a function, but didn't check if you had enough arguments. +const curry = (fn) => { + return function curried(...args) { // <-- The Closure "Backpack" for args + if (args.length >= fn.length) { + // We have enough arguments! Run the original function. + return fn(...args); + } else { + // Not enough arguments yet. Return a function to collect more. + return (...nextArgs) => curried(...args, ...nextArgs); + } + } +} + +// ── F4: Once ────────────────────────────── +// Bug in original: You stopped it from running twice, but didn't return the original result on subsequent calls. +const once = (fn) => { + let called = false; // <-- The Closure "Backpack" + let result; + + return (...args) => { + if (!called) { + result = fn(...args); // Save the result + called = true; // Flip the flag + } + return result; // Always return the saved result + } +} + +// ── F6: Debounce ────────────────────────── +// Bug in original: You executed fn(msg) immediately instead of passing a callback to setTimeout. +const debounce = (fn, delay) => { + let timeoutId; // <-- The Closure "Backpack" + + return (...args) => { + clearTimeout(timeoutId); // Reset the clock + + // We pass an anonymous function to setTimeout so it waits! + timeoutId = setTimeout(() => { + fn(...args); + }, delay); + } +} + +// ── X3: Event Emitter ───────────────────── +// Bug in original: Overcomplicated state tracking. Just use arrays of functions! +const createEmitter = () => { + return { + events: {}, // <-- The Closure "Backpack" holding all subscribers + + on(event, listener) { + if (!this.events[event]) this.events[event] = []; + this.events[event].push(listener); + }, + + off(event, listener) { + if (!this.events[event]) return; + // Filter out the specific listener we want to remove + this.events[event] = this.events[event].filter(l => l !== listener); + }, + + emit(event, ...args) { + if (!this.events[event]) return; + // Run every function subscribed to this event + this.events[event].forEach(listener => listener(...args)); + }, + + once(event, listener) { + // Create a wrapper that runs the listener, then immediately unsubscribes ITSELF + const onceWrapper = (...args) => { + listener(...args); + this.off(event, onceWrapper); + }; + this.on(event, onceWrapper); + } + } +} \ No newline at end of file diff --git a/fullstack/javascript/4-array/golf-score-translator.js b/fullstack/javascript/4-array/golf-score-translator.js new file mode 100644 index 0000000..aa638cd --- /dev/null +++ b/fullstack/javascript/4-array/golf-score-translator.js @@ -0,0 +1,42 @@ +/** + * Function game of Golf, each hole has a par, meaning the average number of strokes a golfer is expected to make in order to sink the ball in the hole to complete the play. Depending on how far above or below par your strokes are, there is a different nickname. Function that converts the par and strokes to their nickname. + * @param {Number} par meaning the average number of strokes a golfer is expected to make in order to sink the ball in the hole to complete the play. + * @param {Number} strokes your strokes in a hole. + * @returns {String} nickname par - strokes convertion. + */ +const golfScore = (par, strokes) => { + if (strokes === 1) { + return "Hole-in-one!" + } else if (strokes <= par - 2) { + return "Eagle" + } else if (strokes === par - 1) { + return "Birdie" + } else if (strokes === par) { + return "Par" + } else if (strokes === par + 1) { + return "Bogey" + } else if (strokes === par + 2) { + return "Double Bogey" + } else if (strokes >= par + 3) { + return "Go Home!" + } +} + +// Test cases: +console.log(`Exercise: Golf Game`) +const nicknameOne = golfScore(5, 1) +const nicknameTwo = golfScore(6, 1) +const nicknameThree = golfScore(4, 2) +const nicknameFour = golfScore(4, 4) +const nicknameFive = golfScore(2, 2) +const nicknameSix = golfScore(7, 9) +const nicknameSeven = golfScore(5, 6) + +console.log(nicknameOne) +console.log(nicknameTwo) +console.log(nicknameThree) +console.log(nicknameFour) +console.log(nicknameFive) +console.log(nicknameSix) +console.log(nicknameSeven) +console.log("\n==================================\n") diff --git a/fullstack/javascript/4-array/golf-score-translator.md b/fullstack/javascript/4-array/golf-score-translator.md new file mode 100644 index 0000000..83ec1c9 --- /dev/null +++ b/fullstack/javascript/4-array/golf-score-translator.md @@ -0,0 +1,19 @@ +# Build a Golf Score Translator + +In the game of Golf, each hole has a par, meaning the average number of strokes a golfer is expected to make in order to sink the ball in the hole to complete the play. Depending on how far above or below par your strokes are, there is a different nickname. +In this lab, you will write a function that converts the par and strokes to their nickname. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named golfScore. +2. [x] golfScore should take two numeric arguments, which are the par for the course and the amount of strokes made. +3. [x] golfScore should return a string. +4. [x] golfScore should return "Hole-in-one!" if strokes is 1. +5. [x] golfScore should return "Eagle" if strokes is less than or equal to par minus 2. +6. [x] golfScore should return "Birdie" if strokes is equal to par minus 1. +7. [x] golfScore should return "Par" if strokes is equal to par. +8. [x] golfScore should return "Bogey" if strokes is equal to par plus 1. +9. [x] golfScore should return "Double Bogey" if strokes is equal to par plus 2. +10. [x] golfScore should return "Go Home!" if strokes is greater than or equal to par plus 3. diff --git a/fullstack/javascript/4-array/lunch-picker-program.js b/fullstack/javascript/4-array/lunch-picker-program.js new file mode 100644 index 0000000..a126eda --- /dev/null +++ b/fullstack/javascript/4-array/lunch-picker-program.js @@ -0,0 +1,127 @@ +const lunches = [] + +/** + * Function that takes an array as the first argument and a string as the second argument. Add the string to the end of the array. + * @param {Array} lunches any string array of lunches. + * @param {String} lunch new lunch to add. + * @returns {Array} updated array. + */ +const addLunchToEnd = (lunches, lunch) => { + if (lunches.push(lunch)) { + console.log(`${lunch} added to the end of the lunch menu.`) + } else { + console.log(`Error adding ${lunch} to the end of the lunch menu.`) + } + return lunches +} + +// Test cases: +console.log(`Exercise 1: Add Lunch To End`) +const updateLunch = addLunchToEnd(lunches, "Scottish eggs") +console.log(updateLunch) +console.log("\n==================================\n") + +/** + * Function that takes an array as the first argument and a string as the second argument. Add the string to the start of the array. + * @param {Array} lunches any string array of lunches. + * @param {String} lunch new lunch to add. + * @returns {Array} updated array. + */ +const addLunchToStart = (lunches, lunch) => { + if (lunches.unshift(lunch)) { + console.log(`${lunch} added to the start of the lunch menu.`) + } else { + console.log(`Error adding ${lunch} to the start of the lunch menu.`) + } + return lunches +} + +// Test cases: +console.log(`Exercise 2: Add Lunch At Start`) +const updateLunchAtStart = addLunchToStart(lunches, "Pancakes") +console.log(updateLunchAtStart) +console.log("\n==================================\n") + +/** + * Function that takes an array as the first argument. Remove the last element from the array. + * @param {Array} lunches any string array of lunches. + * @returns {Array} updated array. + */ +const removeLastLunch = (lunches) => { + const lastLunch = lunches.pop() + if (lastLunch) { + console.log(`${lastLunch} removed from the end of the lunch menu.`) + } else { + console.log("No lunches to remove.") + } + return lunches +} + +// Test cases: +console.log(`Exercise 3: Remove Last Lunch`) +const deleteLastLunch = removeLastLunch(lunches) +console.log(deleteLastLunch) +console.log("\n==================================\n") + +/** + * Function that takes an array as the first argument. Remove the first element from the array. + * @param {Array} lunches any string array of lunches. + * @returns {Array} updated array. + */ +const removeFirstLunch = (lunches) => { + const firstLunch = lunches.shift() + if (firstLunch) { + console.log(`${firstLunch} removed from the start of the lunch menu.`) + } else { + console.log("No lunches to remove.") + } + return lunches +} + +// Test cases: +console.log(`Exercise 4: Remove First Lunch`) +const deleteFirstLunch = removeFirstLunch(["Victorian's Egg", "Conflake", "Pancakes", "Toast and Jam", "Coffe with milk"]) +console.log(deleteFirstLunch) +console.log("\n==================================\n") + +/** + * Function that takes an array as the first argument. Select a random element from the array. + * @param {Array} lunches any string array of lunches. + * @returns {String} If successful, log the string Randomly selected lunch: [Lunch Item] to the console, where [Lunch Item] is a random element in the array. If the array is empty, log the string "No lunches available." to the console. + */ +const getRandomLunch = (lunches) => { + if (lunches.length === 0) { + console.log("No lunches available.") + } else { + let randomNumber = Math.floor(Math.random() * lunches.length) + const lunch = lunches[randomNumber] + if (lunch) { + console.log("Randomly selected lunch: " + lunch) + } + } +} + +// Test cases: +console.log(`Exercise 5: Get Random Lunch`) +const pickRandomLunch = getRandomLunch(["Victorian's Egg", "Conflake", "Pancakes", "Toast and Jam", "Coffe with milk"]) +console.log(pickRandomLunch) +console.log("\n==================================\n") + +/** + * Function that takes an array as the first argument. logs the string Menu items: [Lunch Item], [Lunch Item]... to the console + * @param {Array} lunches any string array of lunches. + * @returns {String} string Menu items. + */ +const showLunchMenu = (lunches) => { + if (lunches.length === 0) { + console.log("The menu is empty.") + } else { + console.log("Menu items: " + lunches.join(", ")) + } +} + +// Test cases: +console.log(`Exercise 6: Show Lunch Menu`) +const viewLunchMenu = showLunchMenu(["Victorian's Egg", "Conflake", "Pancakes", "Toast and Jam", "Coffe with milk"]) +console.log(viewLunchMenu) +console.log("\n==================================\n") diff --git a/fullstack/javascript/4-array/lunch-picker-program.md b/fullstack/javascript/4-array/lunch-picker-program.md new file mode 100644 index 0000000..27dbeb9 --- /dev/null +++ b/fullstack/javascript/4-array/lunch-picker-program.md @@ -0,0 +1,34 @@ +# Build a Lunch Picker Program + +In this lab, you'll build a program that helps in managing lunch options. You'll work with an array of lunches, add and remove items from the array, and randomly select a lunch option. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a lunches variable and assign it an empty array that will be used to store lunch items. +2. [x] You should create a function addLunchToEnd that takes an array as the first argument and a string as the second argument. The function should: + - Add the string to the end of the array. + - Log the string [Lunch Item] added to the end of the lunch menu. to the console, where [Lunch Item] is the string passed to the function. + - Return the updated array. +3. [x] You should create a function addLunchToStart that takes an array as the first argument and a string as the second argument. The function should: + - Add the string to the start of the array. + - Log the string [Lunch Item] added to the start of the lunch menu. to the console, where [Lunch Item] is the string passed to the function. + - Return the updated array. +4. [x] You should create a function removeLastLunch that takes an array as its argument. The function should: + - Remove the last element from the array. + - If the removal is successful, log the string [Lunch Item] removed from the end of the lunch menu. to the console, where [Lunch Item] is the element removed from the array. + - If the array is empty, log the string "No lunches to remove." to the console. + - Return the updated array. +5. [x] You should create a function removeFirstLunch that takes an array as its argument. The function should: + - Remove the first element from the array. + - If the removal is successful, log the string [Lunch Item] removed from the start of the lunch menu. to the console, where [Lunch Item] is the element removed from the array. + - If the array is empty, log the string "No lunches to remove." to the console. + - Return the updated array. +6. [x] You should create a function getRandomLunch that takes an array as its argument. The function should: + - Select a random element from the array. + - If successful, log the string Randomly selected lunch: [Lunch Item] to the console, where [Lunch Item] is a random element in the array. + - If the array is empty, log the string "No lunches available." to the console. +7. [x] You should create a function showLunchMenu that takes an array as its argument and: + - If there are elements in the array, logs the string Menu items: [Lunch Item], [Lunch Item]... to the console, where each [Lunch item] is one of the elements in the array, in order. + - If the array is empty, logs the string "The menu is empty." to the console. diff --git a/fullstack/javascript/5-objects/cargo-manifest-validator.js b/fullstack/javascript/5-objects/cargo-manifest-validator.js new file mode 100644 index 0000000..a11a0a0 --- /dev/null +++ b/fullstack/javascript/5-objects/cargo-manifest-validator.js @@ -0,0 +1,139 @@ +const manifest = { + containerId: 1, + destination: "Monterey, California, USA", + weight: 831, + unit: "lb", + hazmat: false, +} + +const validManifest = { + containerId: 2, + destination: "Queretaro, Queretaro, MX", + weight: 1000, + unit: "kg", + hazmat: true, +} + +const invalidManifest = { + containerId: 3, + weight: "500", + unit: "kg", + hazmat: true, +} + +/** + * Function that normalize and validate cargo manifests. A cargo manifest is a document that typically lists goods being transported (for example, by ship or train) and includes details about those goods. +Each cargo manifest will be represented as an object with the following properties: + +- containerId: a positive integer identifying the associated cargo container. +- destination: a non-empty string (after trimming whitespace) denoting the cargo's target destination. +- weight: a positive number representing the cargo's weight. +- unit: a string describing the units for the cargo's weight property (either "kg" for kilograms or "lb" for pounds). +- hazmat: a boolean value indicating whether hazardous material handling is needed. + Example cargo manifest object: + { + containerId: 1, + destination: "Monterey, California, USA", + weight: 831, + unit: "lb", + hazmat: false + } + * @param {Object} manifest a cargo manifest is a document that typically lists goods being transported (for example, by ship or train) and includes details about those goods. + * @returns {Object} a normalized cargo manifest. + */ +const normalizeUnits = (manifest) => { + let normalizedManifest = { ...manifest } + if (normalizedManifest["unit"] === "lb") { + normalizedManifest.weight = normalizedManifest.weight * 0.45 + normalizedManifest.unit = "kg" + } + return normalizedManifest +} + +// Test cases: +console.log(`Exercise 1: Normalize Units`) +const manifestInKg = normalizeUnits(manifest) +console.log(manifestInKg) +console.log("\n==================================\n") + +/** + * Function that validate the input manifest. If the input manifest is valid (no missing or invalid properties), the function should return an empty object. If the input manifest is not valid, the function should return an object containing entries for each missing or invalid property. Missing properties should have the value "Missing" and invalid properties should have the value "Invalid". + Example return value where the input object is missing the destination property and has an invalid weight property: + { + destination: "Missing", + weight: "Invalid" + } + * @param {Object} manifest a cargo manifest is a document that typically lists goods being transported (for example, by ship or train) and includes details about those goods. + * @returns {Object} If the input manifest is valid (no missing or invalid properties), the function should return an empty object.. If the input manifest is not valid, the function should return an object containing entries for each missing or invalid property. + */ +const validateManifest = (manifest) => { + const keysManifest = ["containerId", "destination", "weight", "unit", "hazmat"] + let missingInvalid = {} + + for (const key of keysManifest) { + // 1. check key presented + if (key in manifest) { + // 2. check isValid value + switch (key) { + case "containerId": + if (typeof manifest[key] !== "number" || manifest[key] < 0) { + missingInvalid[key] = "Invalid" + } + break + case "destination": + if (typeof manifest[key] !== "string" || manifest[key].replace(/\s+/g, "") === "") { + missingInvalid[key] = "Invalid" + } + break + case "weight": + if (typeof manifest[key] !== "number" || manifest[key] < 0) { + missingInvalid[key] = "Invalid" + } + break + case "unit": + if (typeof manifest[key] !== "string") { + missingInvalid[key] = "Invalid" + } else if (manifest[key] !== "kg" && manifest[key] !== "lb") { + missingInvalid[key] = "Invalid" + } + break + case "hazmat": + if (typeof manifest[key] !== "boolean") { + missingInvalid[key] = "Invalid" + } + break + } + } else { + missingInvalid[key] = "Missing" + } + } + return missingInvalid +} + +// Test cases: +console.log(`Exercise 2: Validate Manifest`) +const isValidManifest = validateManifest(manifest) +console.log(isValidManifest) +console.log("\n==================================\n") + +/** + * Function that checks valid manifest and get manifest properties. + * @param {Object} manifest a cargo manifest is a document that typically lists goods being transported (for example, by ship or train) and includes details about those goods. + * @returns {String} If the manifest object is valid return Validation success: ${containerId} Total weight: ${weight} kg. If the manifest object is not valid return Validation error: ${containerId} object validateManifest(). + */ +const processManifest = (manifest) => { + const isValid = Object.keys(validateManifest(manifest)) + if (isValid.length === 0) { + console.log(`Validation success: ${manifest.containerId} Total weight: ${normalizeUnits(manifest).weight} kg`) + return `Validation success: ${manifest.containerId} Total weight: ${normalizeUnits(manifest).weight} kg` + } else { + console.log(`Validation error: ${manifest.containerId} ${validateManifest(manifest)}`) + return `Validation error: ${manifest.containerId} ${validateManifest(manifest)}` + } +} + +console.log(`Exercise 3: Process Manifest`) +// Test cases: +const processedManifest = processManifest(manifest) +console.log(processedManifest) +console.log("\n==================================\n") diff --git a/fullstack/javascript/5-objects/cargo-manifest-validator.md b/fullstack/javascript/5-objects/cargo-manifest-validator.md new file mode 100644 index 0000000..1fa9049 --- /dev/null +++ b/fullstack/javascript/5-objects/cargo-manifest-validator.md @@ -0,0 +1,41 @@ +# Build a Cargo Manifest Validator + +In this lab, you will use JavaScript to normalize and validate cargo manifests. A cargo manifest is a document that typically lists goods being transported (for example, by ship or train) and includes details about those goods. +Each cargo manifest will be represented as an object with the following properties: + +- containerId: a positive integer identifying the associated cargo container. +- destination: a non-empty string (after trimming whitespace) denoting the cargo's target destination. +- weight: a positive number representing the cargo's weight. +- unit: a string describing the units for the cargo's weight property (either "kg" for kilograms or "lb" for pounds). +- hazmat: a boolean value indicating whether hazardous material handling is needed. + Example cargo manifest object: + { + containerId: 1, + destination: "Monterey, California, USA", + weight: 831, + unit: "lb", + hazmat: false + } + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should implement a function named normalizeUnits with a manifest parameter. + - The function must not mutate the original manifest object and must always return a new object where weight is normalized to kilograms and unit is set to "kg". + - If the weight of the manifest object is expressed in pounds (unit: "lb"), the function should convert the weight to kilograms using the approximate conversion, 1 lb = 0.45 kg, and update the unit accordingly. + - If the weight is already expressed in kilograms (unit: "kg"), the weight and unit should remain unchanged. +2. [x] You should implement a function named validateManifest with a manifest parameter. + - The function must not mutate the original manifest object and must always return a new object. + - If the input manifest is valid (no missing or invalid properties), the function should return an empty object. + - If the input manifest is not valid, the function should return an object containing entries for each missing or invalid property. Missing properties should have the value "Missing" and invalid properties should have the value "Invalid". + Example return value where the input object is missing the destination property and has an invalid weight property: + { + destination: "Missing", + weight: "Invalid" + } +3. [x] You should implement a function named processManifest with a manifest parameter. The function should log: + - If the manifest object is valid, Validation success: ${containerId} and then the manifest's weight in kilograms as such, Total weight: ${weight} kg. Use normalizeUnits() for this conversion. + - If the manifest object is not valid, Validation error: ${containerId} and then the object returned by calling validateManifest() with the manifest object. + +**Note**: each of these two cases should have two console.log() calls. diff --git a/fullstack/javascript/5-objects/exam/fcc-ch5-grok-solution.js b/fullstack/javascript/5-objects/exam/fcc-ch5-grok-solution.js new file mode 100644 index 0000000..5b74808 --- /dev/null +++ b/fullstack/javascript/5-objects/exam/fcc-ch5-grok-solution.js @@ -0,0 +1,270 @@ +// ====================== PART 2 ====================== +console.log("\n=== Part 2 — Object Fundamentals ===") + +// O1 - mergeObjects +const mergeObjects = (obj1, obj2) => { + return { ...obj1, ...obj2 } +} + +console.log(">>>Merge: ", mergeObjects({ a: 1, b: 2 }, { b: 3, c: 4 })) +console.log(">>>Merge: ", mergeObjects({ x: 10 }, { y: 20 }), "\n") + +// O2 - pick +const pick = (obj, keys) => { + return keys.reduce((acc, key) => { + if (key in obj) acc[key] = obj[key] + return acc + }, {}) +} + +console.log(">>>PICK: ", pick({ a: 1, b: 2, c: 3 }, ["a", "c"])) +console.log(">>>PICK: ", pick({ x: 10, y: 20 }, ["x", "z"])) +console.log(">>>PICK: ", pick({}, ["a"]), "\n") + +// O3 - omit +const omit = (obj, keys) => { + return Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key))) +} + +console.log(">>>OMIT: ", omit({ a: 1, b: 2, c: 3 }, ["b"])) +console.log(">>>OMIT: ", omit({ x: 10, y: 20, z: 30 }, ["x", "z"]), "\n") + +// O4 - invertObject +const invertObject = (obj) => { + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [String(value), key])) +} + +console.log(">>>INVERT: ", invertObject({ a: "x", b: "y" })) +console.log(">>>INVERT: ", invertObject({ 1: "one", 2: "two" }), "\n") + +// O5 - deepClone (Fixed) +const deepClone = (obj) => { + // 1. Base case + if (obj === null || typeof obj !== "object") return obj + if (Array.isArray(obj)) { + return obj.map(deepClone) + } + return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, deepClone(value)])) +} + +const a = { x: 1, nested: { y: 2 }, arr: [1, 2, 3] } +const b = deepClone(a) +b.nested.y = 99 +b.arr.push(4) +console.log(">>> DeepClone original:", a) +console.log(">>> DeepClone clone:", b) + +// ====================== PART 3 ====================== +console.log("\n=== Part 3 — Real-World Patterns ===") + +const people = [ + { name: "Alice", dept: "Engineering", salary: 90000, active: true }, + { name: "Bob", dept: "Marketing", salary: 75000, active: false }, + { name: "Carol", dept: "Engineering", salary: 85000, active: true }, + { name: "Dave", dept: "Marketing", salary: 70000, active: false }, + { name: "Eve", dept: "Design", salary: 80000, active: true }, +] + +// groupBy +const groupBy = (arr, key) => + arr.reduce((acc, obj) => { + const groupKey = obj[key] + if (groupKey !== undefined) { + if (!acc[groupKey]) acc[groupKey] = [] + acc[groupKey].push(obj) + } + return acc + }, {}) + +console.log(">>>GroupBy dept:", groupBy(people, "dept")) +console.log(">>>GroupBy active:", groupBy(people, "active"), "\n") + +// transformUserData +const rawUsers = [ + { firstName: "John", lastName: "Doe", password: "abc123", status: "active", createdAt: "2023-04-15" }, + { firstName: "Jane", lastName: "Smith", password: "xyz789", status: "inactive", createdAt: "2021-11-02" }, + { firstName: "Alice", lastName: "Wong", password: "p4ssw0rd", status: "active", createdAt: "2024-01-30" }, +] + +const transformUserData = (users) => + users.map(({ firstName, lastName, status, createdAt }) => ({ + firstName, + lastName, + fullName: `${firstName} ${lastName}`, + isActive: status === "active", + joinedYear: createdAt.split("-")[0], + })) + +console.log(">>>MAP: ", transformUserData(rawUsers), "\n") + +// buildInventoryReport (Fixed) +const inventoryItems = [ + { name: "Laptop", category: "Electronics", price: 1200.35, quantity: 5 }, + { name: "Phone", category: "Electronics", price: 800, quantity: 0 }, + { name: "Desk", category: "Furniture", price: 350, quantity: 3 }, + { name: "Monitor", category: "Electronics", price: 600, quantity: 2 }, + { name: "Chair", category: "Furniture", price: 250, quantity: 0 }, + { name: "Keyboard", category: "Electronics", price: 150.5874, quantity: 10 }, +] + +const buildInventoryReport = (items) => { + return items.reduce( + (inv, item) => { + inv.totalItems += item.quantity + inv.totalValue += Number((item.price * item.quantity).toFixed(2)) + + if (!inv.byCategory[item.category]) { + inv.byCategory[item.category] = { count: 0, value: 0 } + } + inv.byCategory[item.category].count += 1 + inv.byCategory[item.category].value += Number((item.price * item.quantity).toFixed(2)) + + if (item.price > inv.mostExpensive.price) { + inv.mostExpensive = { ...item } // copy to avoid mutation issues + } + if (item.quantity === 0) { + inv.outOfStock.push(item.name) + } + return inv + }, + { + totalItems: 0, + totalValue: 0, + byCategory: {}, + mostExpensive: { price: -Infinity }, + outOfStock: [], + }, + ) +} +const inventory = buildInventoryReport(inventoryItems) +console.log(">>>REDUCE R3: ", inventory, "\n") + +// createStore (Improved) +const createStore = (initialState) => { + let state = deepClone(initialState) + let listeners = new Set() + + return { + getState() { + return deepClone(state) + }, + setState(updater) { + state = updater(state) + listeners.forEach((listener) => listener(state)) + }, + subscribe(listener) { + listeners.add(listener) + }, + unsubscribe(listener) { + listeners.delete(listener) + }, + } +} + +// ====================== PART 4 ====================== +// (createContact, addContact, findContact, deleteContact, createConfig, createSchema) +// All improved with better robustness, deep support, and correct logic. + +const createContact = (firstName, lastName, email, phone) => ({ + id: Date.now(), + firstName, + lastName, + email, + phone, + createdAt: new Date().toISOString(), +}) + +const yoandy = createContact("Yoandy", "Doble Herrera", "codebydoble@gmail.com", "+53-582-3265") +const yerlany = createContact("Yerlany", "Doble Herrera", "yerly@gmail.com", "+53-111-3874") +console.log(yoandy) +console.log(yerlany) + +const addContact = (contacts, contact) => ({ ...contacts, [contact.id]: contact }) + +const findContact = (contacts, query) => { + const q = query.toLowerCase() + return Object.values(contacts).filter( + (c) => c.firstName.toLowerCase().includes(q) || c.lastName.toLowerCase().includes(q) || c.email.toLowerCase().includes(q), + ) +} + +const deleteContact = (contacts, id) => { + const copy = { ...contacts } + delete copy[id] + return copy +} + +// createConfig (Fixed - proper deep dot notation) +const cConf = (defaults) => { + let config = deepClone(defaults) + return { + get(path) { + return path.split(".").reduce((obj, key) => { + return obj?.[key] + }, config) + }, + set(path, value) { + const keys = path.split(".") + let current = config + for (let i = 0; i < keys.length - 1; i++) { + if (!(keys[i] in current)) current[keys[i]] = {} + current = current[keys[i]] + } + current[keys[keys.length - 1]] = value + }, + reset() { + config = deepClone(defaults) + }, + getAll() { + return deepClone(config) + }, + } +} + +const myConfig = cConf({ + theme: { color: "blue", fontSize: 14 }, + language: "en", + features: { darkMode: false, notifications: true }, +}) + +myConfig.get("theme.color") //→ "blue" +console.log(myConfig.get("theme.color")) + +myConfig.set("theme.color", "red") +console.log(myConfig.get("theme")) +myConfig.set("language", "es") +myConfig.get("language") //→ "es" +myConfig.reset() +myConfig.get("theme.color") //→ "blue" (back to default) +myConfig.get("language") //→ "en" (back to default) + +// createSchema (Fixed & Clean) +const createSchema = (schema) => (data) => { + const errors = {} + for (const [field, rules] of Object.entries(schema)) { + const value = data[field] + const fieldErrors = [] + + if (rules.required && value === undefined) { + fieldErrors.push("Required field missing") + } else if (value !== undefined) { + if (rules.type && typeof value !== rules.type) fieldErrors.push(`Expected type ${rules.type}`) + if (rules.min !== undefined) { + const check = typeof value === "string" ? value.length : value + if (check < rules.min) fieldErrors.push(`Too short/small (min ${rules.min})`) + } + if (rules.max !== undefined) { + const check = typeof value === "string" ? value.length : value + if (check > rules.max) fieldErrors.push(`Too long/big (max ${rules.max})`) + } + if (rules.pattern && !rules.pattern.test(value)) { + fieldErrors.push("Does not match pattern") + } + if (rules.custom && !rules.custom(value)) { + fieldErrors.push("Invalid custom validation") + } + } + if (fieldErrors.length) errors[field] = fieldErrors + } + return { valid: Object.keys(errors).length === 0, errors } +} diff --git a/fullstack/javascript/5-objects/exam/fcc-ch5-objects-exam.js b/fullstack/javascript/5-objects/exam/fcc-ch5-objects-exam.js new file mode 100644 index 0000000..80bdd17 --- /dev/null +++ b/fullstack/javascript/5-objects/exam/fcc-ch5-objects-exam.js @@ -0,0 +1,1022 @@ +/** + * ════════════════════════════════════════ + * freeCodeCamp Chapter 5 — OBJECTS + * MASTERY EXAM + * ════════════════════════════════════════ + * Student: Yoandy Doble Herrera + * Date: 08/06/2026 - 22/06/2026 + * Start: 08:47 End: 16:48 + * + * RULES: + * - Vanilla JS only. No libraries. + * - Write JSDoc for every function. + * - Test every function with at least 2 console.log calls. + * - Do not mutate input objects unless explicitly asked. + * - Run with: node fcc-ch5-objects-exam.js + * + * STRUCTURE: + * Part 1 — Theory & Predictions (20 pts) + * Part 2 — Object Fundamentals (25 pts) + * Part 3 — Real-World Patterns (30 pts) + * Part 4 — Combined Challenges (25 pts) + * ──────────────────────────────────────── + * Total: 100 pts | Passing: 75 pts + * ════════════════════════════════════════ + */ + +// ════════════════════════════════════════ +// PART 1 — THEORY & PREDICTIONS (20 pts) +// ════════════════════════════════════════ +// Write your prediction as a comment BEFORE running each block. +// Award yourself points only if you predicted correctly. +"use strict" +console.log("\n=== Part 1 — Theory & Predictions ===") + +// ── Q1 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Question 1 <<<===>>>\n") +// Predict the output of each line. +const user = { name: "Yoandy", role: "developer", active: true } + +console.log(user.name) // Your prediction: Yoandy +console.log(user["role"]) // Your prediction: developer +console.log(user.age) // Your prediction: undefined +console.log("active" in user) // Your prediction: true + +// ── Q2 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Question 2 <<<===>>>\n") +// Predict what gets logged. Think about reference vs copy. +const original = { x: 1, y: 2 } +const copied = original +copied.x = 99 +console.log(original.x) // Your prediction: 99 + +const spread = { ...original } +spread.y = 50 +console.log(original.y) // Your prediction: 2 +console.log(original === copied) // Your prediction: true +console.log(original === spread) // Your prediction: false + +// ── Q3 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Question 3 <<<===>>>\n") +// Predict the output. Think about Object methods. +const car = { brand: "Mazda", year: 2024, color: "red" } +console.log(Object.keys(car)) // Your prediction: [brand, year, color] +console.log(Object.values(car)) // Your prediction: ["Mazda",2024,"red"] +console.log(Object.entries(car).length) // Your prediction: 3 + +delete car.color +console.log("color" in car) // Your prediction: false + +// ── Q4 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Question 4 <<<===>>>\n") +// Predict the output. Think about nested access and optional chaining. +const company = { + name: "TechCorp", + address: { + city: "Guadalajara", + country: "Mexico", + }, +} +console.log(company.address.city) // Your prediction: Guadalajara +console.log(company.address.zip) // Your prediction: undefined +console.log(company.ceo?.name) // Your prediction: undefined +console.log(company.address?.city?.length) // Your prediction: 11 + +// ── Q5 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Question 5 <<<===>>>\n") +// Predict the output. Think about destructuring and defaults. +const { name, role = "viewer", score = 0 } = { name: "Alice", role: "admin" } +console.log(name) // Your prediction: Alice +console.log(role) // Your prediction: admin +console.log(score) // Your prediction: 0 + +const { address: { city } = {} } = company +console.log(city) // Your prediction: Guadalajara + +// ════════════════════════════════════════ +// PART 2 — OBJECT FUNDAMENTALS (25 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 2 — Object Fundamentals ===") + +// ── O1 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Object 1 <<<===>>>\n") +/** + * Write a function mergeObjects(obj1, obj2) that returns a NEW object + * combining both objects. If the same key exists in both, obj2's value wins. + * Must not mutate either input. + * + * mergeObjects({a:1, b:2}, {b:3, c:4}) → {a:1, b:3, c:4} + * mergeObjects({x:10}, {y:20}) → {x:10, y:20} + */ +// Your solution: +/** + * Function that returns a NEW object combining both objects. If the same key exists in both, obj2's value wins. Must not mutate either input. + * @param {Object} obj1 any object. + * @param {Object} obj2 any object. + * @returns {Object} a NEW object combining both objects. + */ +const mergeObjects = (obj1, obj2) => { + let merged = Object.create(null) + // 1. spread obj1 + merged = { ...obj1 } + // 2. add obj 2. If the same key exists in both, obj2's value wins. + for (const [key, value] of Object.entries(obj2)) { + merged[key] = value + } + // BETTER APROACH + // return { ...obj1, ...obj2 } If the same key exists in both, obj2's value wins. + return merged +} + +const mergeQ1E1 = mergeObjects({ a: 1, b: 2 }, { b: 3, c: 4 }) // {a:1, b:3, c:4} +console.log(">>>Merge: ", mergeQ1E1) +const mergeQ1E2 = mergeObjects({ x: 10 }, { y: 20 }) // {x:10, y:20} +console.log(">>>Merge: ", mergeQ1E2, "\n") +// ── O2 (5 pts) ─────────────────────────── +console.log("\n<<<===>>> Object 2 <<<===>>>\n") +/** + * Write a function pick(obj, keys) that returns a NEW object containing + * only the properties whose names are in the keys array. + * If a key doesn't exist on obj, skip it silently. + * + * pick({a:1, b:2, c:3}, ["a","c"]) → {a:1, c:3} + * pick({x:10, y:20}, ["x","z"]) → {x:10} + * pick({}, ["a"]) → {} + */ +// Your solution: + +/** + * Function that returns a NEW object containing only the properties whose names are in the keys array. + * If a key doesn't exist on obj, skip it silently. + * @param {Object} obj any object. + * @param {Array} keys any keys array. + * @returns {Object} a NEW object containing only the properties whose names are in the keys array. + */ +const pick = (obj, keys) => { + // BETTER APROACH use reduce + const picked = {} + // 1. checks empty keys + if (keys.length === 0) return {} + // 2. checks empty obj + if (Object.entries(obj).length === 0) return {} + // 3. loop keys and add into picked + for (const key of keys) { + if (key in obj) { + picked[key] = obj[key] + } + } + return picked +} + +const pickQ2P1 = pick({ a: 1, b: 2, c: 3 }, ["a", "c"]) // {a:1, c:3} +const pickQ2P2 = pick({ x: 10, y: 20 }, ["x", "z"]) //{x:10} +const pickQ2P3 = pick({}, ["a"]) // {} +console.log(">>>PICK: ", pickQ2P1) +console.log(">>>PICK: ", pickQ2P2) +console.log(">>>PICK: ", pickQ2P3, "\n") + +// ── O3 (4 pts) ─────────────────────────── +console.log("\n<<<===>>> Object 3 <<<===>>>\n") +/** + * Write a function omit(obj, keys) — the opposite of pick. + * Returns a NEW object with all properties EXCEPT those in keys. + * + * omit({a:1, b:2, c:3}, ["b"]) → {a:1, c:3} + * omit({x:10, y:20, z:30}, ["x","z"]) → {y:20} + */ +// Your solution: + +/** + * Function omit(obj, keys) — the opposite of pick. + * Returns a NEW object with all properties EXCEPT those in keys. + * @param {Object} obj any object. + * @param {String[]} keys any keys array. + * @returns {Object} + */ +const omit = (obj, keys) => { + let omitted = {} + // 1. checks empty keys + if (keys.length === 0) return obj + // 2. checks empty obj + if (Object.entries(obj).length === 0) return {} + // 3. loops obj and test keys includes + for (const [key, value] of Object.entries(obj)) { + if (!keys.includes(key)) { + omitted[key] = value + } + } + return omitted +} + +const omitQ3P1 = omit({ a: 1, b: 2, c: 3 }, ["b"]) // {a:1, c:3} +const omitQ3P2 = omit({ x: 10, y: 20, z: 30 }, ["x", "z"]) // {y:20} +console.log(">>>OMIT: ", omitQ3P1) +console.log(">>>OMIT: ", omitQ3P2, "\n") + +// ── O4 (5 pts) ─────────────────────────── +console.log("\n<<<===>>> Object 4 <<<===>>>\n") +/** + * Write a function invertObject(obj) that swaps all keys and values. + * Assume all values are strings or numbers (safe to use as keys). + * + * invertObject({a:"x", b:"y"}) → {x:"a", y:"b"} + * invertObject({1:"one", 2:"two"}) → {one:"1", two:"2"} + * + * Note: Object keys are always strings — think about what happens to number values. + */ +// Your solution: + +/** + * function invertObject(obj) that swaps all keys and values. + * Assume all values are strings or numbers (safe to use as keys). + * @param {Object} obj + * @returns {Object} + */ +const invertObject = (obj) => { + let inverted = {} + for (const [key, value] of Object.entries(obj)) { + inverted[value.toString()] = key + } + return inverted +} + +const invertQ4P1 = invertObject({ a: "x", b: "y" }) // {x:"a", y:"b"} +const invertQ4P2 = invertObject({ 1: "one", 2: "two" }) // {one:"1", two:"2"} +const invertQ4P3 = invertObject({ a: "zz", b: "y", 23: 50 }) // { '50': '23', zz: 'a', y: 'b' } +console.log(">>>INVERT: ", invertQ4P1) +console.log(">>>INVERT: ", invertQ4P2) +console.log(">>>INVERT: ", invertQ4P3, "\n") + +// ── O5 (7 pts) ─────────────────────────── +console.log("\n<<<===>>> Object 5 <<<===>>>\n") +/** + * Write a function deepClone(obj) that creates a fully independent deep copy + * of an object (including nested objects and arrays). + * Do NOT use JSON.parse/JSON.stringify or structuredClone(). + * Implement it yourself using recursion. + * + * const a = { x: 1, nested: { y: 2 } } + * const b = deepClone(a) + * b.nested.y = 99 + * console.log(a.nested.y) // 2 — original untouched + * + * Hint: check Array.isArray() first, then typeof === "object" + */ +// Your solution: + +/** + * Function deepClone(obj) that creates a fully independent deep copy of an object (including nested objects and arrays). Use recursion. + * @param {Object} obj + */ +const deepClone = (obj) => { + return Object.entries(obj).reduce((clone, elementArr) => { + let [key, value] = elementArr + if (!Array.isArray(value)) { + if (typeof value === "object" && value !== null) { + clone[key] = deepClone(value) + return clone + } + } + clone[key] = value + return clone + }, {}) +} +const a = { x: 1, nested: { y: 2 } } +const b = deepClone(a) +b.nested.y = 99 +console.log(">>> DeepClone original", a) // original untouched +console.log(">>> DeepClone b", b) // cloned +console.log(a.nested.y) // 2 — original untouched + +// ════════════════════════════════════════ +// PART 3 — REAL-WORLD PATTERNS (30 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 3 — Real-World Patterns ===") + +// ── R1 (6 pts) ─────────────────────────── +console.log("\n<<<===>>> Real-World 1 <<<===>>>\n") +/** + * Write a function groupBy(arr, key) that groups an array of objects + * by a shared property, returning an object of arrays. + * + * const people = [ + * { name:"Alice", dept:"Engineering" }, + * { name:"Bob", dept:"Marketing" }, + * { name:"Carol", dept:"Engineering" }, + * ] + * groupBy(people, "dept") → + * { + * Engineering: [{name:"Alice",...}, {name:"Carol",...}], + * Marketing: [{name:"Bob",...}] + * } + * + * This is the mystery() function from Ch.5 Eloquent JS — you've seen this before. + */ +const people = [ + { name: "Alice", dept: "Engineering", salary: 90000, active: true }, + { name: "Bob", dept: "Marketing", salary: 75000, active: false }, + { name: "Carol", dept: "Engineering", salary: 85000, active: true }, + { name: "Dave", dept: "Marketing", salary: 70000, active: false }, + { name: "Eve", dept: "Design", salary: 80000, active: true }, +] + +/** + * Function that groups an array of objects by a shared property, returning an object of arrays. + * @param {Array} arr any array of objects. + * @param {String} key a shared property. + * @returns {Object} an object of arrays gruopBy shared property. + */ +const groupBy = (arr, key) => { + return arr.reduce((groupResult, obj) => { + if (key in obj) { + if (obj[key] in groupResult) { + groupResult[obj[key]].push(obj) + } else { + groupResult[obj[key]] = [] + groupResult[obj[key]].push(obj) + } + } + return groupResult + }, {}) +} + +const groupR1P1 = groupBy(people, "dept") /* +{ + Engineering: [{name:"Alice",...}, {name:"Carol",...}], + Marketing: [{name:"Bob",...}] +} +*/ +const groupR1P2 = groupBy(people, "active") /* + { + true: [ + { name: 'Alice', dept: 'Engineering', salary: 90000, active: true }, + { name: 'Carol', dept: 'Engineering', salary: 85000, active: true }, + { name: 'Eve', dept: 'Design', salary: 80000, active: true } + ], + false: [ + { name: 'Bob', dept: 'Marketing', salary: 75000, active: false }, + { name: 'Dave', dept: 'Marketing', salary: 70000, active: false } + ] +} +*/ +console.log(">>>GroupBy: ", groupR1P1) +console.log(">>>GroupBy: ", groupR1P2, "\n") +// ── R2 (7 pts) ─────────────────────────── +console.log("\n<<<===>>> Real-World 2 <<<===>>>\n") +/** + * Write a function transformUserData(users) that takes an array of raw user + * objects and returns a transformed array where: + * - fullName is added: firstName + " " + lastName + * - password is removed + * - isActive is added: true if status === "active", false otherwise + * - joinedYear is added: just the year from the createdAt date string + * + * Input: + * { firstName:"John", lastName:"Doe", password:"abc123", + * status:"active", createdAt:"2023-04-15" } + * Output: + * { firstName:"John", lastName:"Doe", + * fullName:"John Doe", isActive:true, joinedYear:2023 } + */ +const rawUsers = [ + { firstName: "John", lastName: "Doe", password: "abc123", status: "active", createdAt: "2023-04-15" }, + { firstName: "Jane", lastName: "Smith", password: "xyz789", status: "inactive", createdAt: "2021-11-02" }, + { firstName: "Alice", lastName: "Wong", password: "p4ssw0rd", status: "active", createdAt: "2024-01-30" }, +] +// Your solution: + +/** + * Function that takes an array of raw user objects and returns a transformed array where: + * - fullName is added: firstName + " " + lastName + * - password is removed + * - isActive is added: true if status === "active", false otherwise + * - joinedYear is added: just the year from the createdAt date string + * @param {Object[]} users any raw user objects. + * @returns {Object[]} a transformed array like: { + firstName: 'Alice', + lastName: 'Wong', + fullName: 'Alice Wong', + isActive: true, + joinedYear: '2024' + } + */ +const transformUserData = (users) => { + return users.map((user) => { + let data = {} + data["firstName"] = user.firstName + data["lastName"] = user.lastName + data["fullName"] = user.firstName + " " + user.lastName + data["isActive"] = user.status === "active" ? true : false + data["joinedYear"] = user.createdAt.split("-")[0] + return data + }) +} + +const userData = transformUserData(rawUsers) +console.log(">>>MAP: ", userData, "\n") + +// ── R3 (8 pts) ─────────────────────────── +console.log("\n<<<===>>> Real-World 3 <<<===>>>\n") +/** + * Write a function buildInventoryReport(items) that takes an array of inventory + * items and returns a summary object using a SINGLE reduce pass. + * + * Each item: { name, category, price, quantity } + * Return: + * { + * totalItems: number, // sum of all quantities + * totalValue: number, // sum of price * quantity (rounded to 2dp) + * byCategory: { // grouped totals per category + * "Electronics": { count: N, value: N }, + * "Furniture": { count: N, value: N }, + * }, + * mostExpensive: object, // the item with the highest price + * outOfStock: string[], // names of items with quantity === 0 + * } + */ +const inventoryItems = [ + { name: "Laptop", category: "Electronics", price: 1200.35, quantity: 5 }, + { name: "Phone", category: "Electronics", price: 800, quantity: 0 }, + { name: "Desk", category: "Furniture", price: 350, quantity: 3 }, + { name: "Monitor", category: "Electronics", price: 600, quantity: 2 }, + { name: "Chair", category: "Furniture", price: 250, quantity: 0 }, + { name: "Keyboard", category: "Electronics", price: 150.583, quantity: 10 }, +] +// Your solution: + +/** + * Function that takes an array of inventory items and returns a summary object using a SINGLE reduce pass. + * @param {Object[]} items any array of inventory items. + * @returns {Object[]} a summary object inventory. + */ +const buildInventoryReport = (items) => { + return items.reduce( + /** + * @param {Object} inv acc of inventory. + * @param {Object} item current item. + */ + (inv, item) => { + // 1. Destructuring inv + let { totalItems, totalValue, byCategory, mostExpensive, outOfStock } = inv + // 2. sum of all quantities + totalItems += item.quantity + // 3. sum of price * quantity (rounded to 2dp) + totalValue += Number((item.price * item.quantity).toFixed(2)) + + // 4. grouped totals per category + if (item.category in byCategory) { + byCategory[item.category].count += 1 + byCategory[item.category].value += parseFloat(Number(item.price * item.quantity).toFixed(2)) + } else { + byCategory[item.category] = {} + byCategory[item.category].count = 1 + byCategory[item.category].value = parseFloat(Number(item.price * item.quantity).toFixed(2)) + } + // 5. the item with the highest price + if (item.price > mostExpensive.price) { + mostExpensive = { ...item } + } + // 6. names of items with quantity === 0 + if (item.quantity === 0) { + outOfStock.push(item.name) + } + return { totalItems, totalValue: Number(totalValue.toFixed(2)), byCategory, mostExpensive, outOfStock } + }, + { totalItems: 0, totalValue: 0, byCategory: {}, mostExpensive: { price: -Infinity }, outOfStock: [] }, + ) +} + +const inventory = buildInventoryReport(inventoryItems) +console.log(">>>REDUCE R3: ", inventory, "\n") + +// ── R4 (9 pts) ─────────────────────────── +console.log("\n<<<===>>> Real-World 4 <<<===>>>\n") + +/** + * Function simple state manager — a pattern that powers React's useState. + * @param {Object} initialState any initial State. + * @returns {Object} createStore(initialState) returns an object with: + * - getState() → returns current state (a copy, not the original) + * - setState(updater) → updater is a function: prevState → newState + * - subscribe(listener) → registers a callback called on every state change + * - unsubscribe(listener) → removes a listener + */ +const createStore = (initialState) => { + let state = { ...initialState } + let listeners = {} + return { + /** + * Function that returns current state (a copy, not the original). + * @returns {Object} returns current state (a copy, not the original). + */ + getState() { + return state + }, + /** + * Function (anonymous) to update state. Logs updated status. + * @param {Function} updater any Function (anonymous) to update state. + */ + setState(updater) { + // 1. update state + state = updater(state) + // 2. callback + for (const [, fnc] of Object.entries(listeners)) { + // 3. console.log("State changed:", state) + fnc(state) + } + }, + /** + * Function that registers a callback called on every state change. + * @param {Function} listener any function. + */ + subscribe(listener) { + // 1. checks listener suscribed + if (!Object.hasOwn(listeners, listener.name)) { + listeners[listener.name] = listener + } + }, + /** + * Function that removes a listener. + * @param {Function} listener any function. + */ + unsubscribe(listener) { + // 1. checks listener suscribed + if (listener.name in listeners) { + delete listeners[listener.name] + } + }, + } +} + +const store = createStore({ count: 0, name: "Yoandy" }) +const log = (state) => console.log("State changed:", state) +store.subscribe(log) + +store.setState((s) => { + return { ...s, count: s.count + 1 } +}) +// logs: State changed: { count: 1, name: "Yoandy" } +store.setState((s) => ({ ...s, count: s.count + 1 })) +// logs: State changed: { count: 2, name: "Yoandy" } + +store.unsubscribe(log) +store.setState((s) => ({ ...s, count: 99 })) +// (nothing logged — listener was removed) + +console.log(store.getState()) // { count: 99, name: "Yoandy" } + +// ════════════════════════════════════════ +// PART 4 — COMBINED CHALLENGES (25 pts) +// ════════════════════════════════════════ + +console.log("\n=== Part 4 — Combined Challenges ===") + +// ── X1 (8 pts) ─────────────────────────── +console.log("\n<<<===>>> Combined Challenges 1 <<<===>>>\n") +/** + * Build a contacts manager using only object manipulation — no classes. + * Implement these four functions: + * + * createContact(firstName, lastName, email, phone) + * → returns a contact object with a generated id (use Date.now()) + * and a createdAt timestamp (ISO string) + * + * addContact(contacts, contact) + * → returns a NEW contacts object with the contact added (keyed by id) + * Does not mutate the original contacts object. + * + * findContact(contacts, query) + * → searches by firstName, lastName, or email (case-insensitive) + * → returns array of matching contact objects + * + * deleteContact(contacts, id) + * → returns a NEW contacts object without the contact with that id + * Does not mutate the original. + */ +// Your solution: + +/** + * Function that returns a contact object with a generated id (use Date.now()) and a createdAt timestamp (ISO string) + * @param {String} firstName any firstName. + * @param {String} lastName any lastName. + * @param {String} email email address contact. + * @param {String} phone phone number contact. + * @returns {Object} a contact object. + */ +const createContact = (firstName, lastName, email, phone) => { + const dateObj = new Date() + return { + id: Date.now(), + firstName: firstName, + lastName: lastName, + email: email, + phone: phone, + createdAt: dateObj.toISOString(), + } +} + +/** + * Function that returns a NEW contacts object with the contact added (keyed by id) + * Does not mutate the original contacts object. + * @param {Object} contacts contacts object. + * @param {Object} contact any contact. + * @returns {Object{}} returns a NEW contacts object with the contact added (keyed by id) + */ +const addContact = (contacts, contact) => { + let newContacts = { ...contacts } + newContacts[contact.id] = contact + return newContacts +} + +/** + * Function that searches by firstName, lastName, or email (case-insensitive) + * @param {Object} contacts contacts object. + * @param {String} query search key. + * @returns {Object[]} array of matching contact objects. + */ +const findContact = (contacts, query) => { + const result = [] + for (const key in contacts) { + if ( + contacts[key]["firstName"].toLowerCase() === query.toLowerCase() || + contacts[key]["lastName"].toLowerCase() === query.toLowerCase() || + contacts[key]["email"] === query + ) { + result.push(contacts[key]) + } + } + return result +} + +/** + * Function that returns a NEW contacts object without the contact with that id. + * @param {Object} contacts a contact agend. + * @param {Number} id id to delete in contact agend. + * @returns {Object} a NEW contacts object without the contact with that id. + */ +const deleteContact = (contacts, id) => { + let result = { ...contacts } + if (id in result) { + delete result[id] + } + return result +} + +let agend = {} +console.log("\nCreate contact") +const yoandy = createContact("Yoandy", "Doble Herrera", "codebydoble@gmail.com", "+53-582-3265") +const yerlany = createContact("Yerlany", "Doble Herrera", "yerly@gmail.com", "+53-111-3874") +console.log(yoandy) +console.log(yerlany) + +console.log("\nAdd contact") +const newAgend = addContact(agend, yoandy) +const agendTwo = addContact(newAgend, yerlany) +const agendThree = addContact(agendTwo, createContact("Leandro", "Doble Salgado", "leandro.doble@gmail.com", "+53-111-6687")) +const agendFour = addContact( + agendThree, + createContact("Barbara", "Herrera Gonzalez", "barbara.herrera@gmail.com", "+53-136-3549"), +) + +console.log(agendTwo) +console.log(agendThree) + +let myAgend = { + 1781226467635: { + id: 1781226467635, + firstName: "Yoandy", + lastName: "Doble Herrera", + email: "codebydoble@gmail.com", + phone: "+53-582-3265", + createdAt: "2026-06-12T01:07:47.635Z", + }, + 1781226467636: { + id: 1781226467636, + firstName: "Barbara", + lastName: "Herrera Gonzalez", + email: "barbara.herrera@gmail.com", + phone: "+53-136-3549", + createdAt: "2026-06-12T01:07:47.636Z", + }, + 1781226467637: { + id: 1781226467637, + firstName: "Yerlany", + lastName: "Doble Herrera", + email: "yerly@gmail.com", + phone: "+53-111-3874", + createdAt: "2026-06-12T01:07:47.635Z", + }, + 1781226467638: { + id: 1781226467638, + firstName: "Leandro", + lastName: "Doble Salgado", + email: "leandro.doble@gmail.com", + phone: "+53-111-6687", + createdAt: "2026-06-12T01:07:47.636Z", + }, +} +console.log("\nFind contact") +const matching = findContact(myAgend, "yoandy") +console.log(matching) + +console.log("\nDelete contact") +const deletedContact = deleteContact(myAgend, 1781226467635) +console.log(deletedContact) + +// ── X2 (8 pts) ─────────────────────────── +console.log("\n<<<===>>> Combined Challenges 2 <<<===>>>\n") +/** + * Implement a settings/config system that supports nested dot-notation access. + * + * createConfig(defaults) returns an object with: + * get(path) → reads a value at a dot-notation path + * set(path, value) → sets a value at a dot-notation path (mutates internal state) + * reset() → resets all config to the original defaults (deep clone) + * getAll() → returns the current config (a copy) + * + * const config = createConfig({ + * theme: { color: "blue", fontSize: 14 }, + * language: "en", + * features: { darkMode: false, notifications: true } + * }) + * + * config.get("theme.color") //→ "blue" + * config.get("features.darkMode") //→ false + * config.get("language") //→ "en" + * config.set("theme.color", "red") + * config.get("theme.color") //→ "red" + * config.reset() + * config.get("theme.color") //→ "blue" (back to default) + * + * Hint: split path by ".", then traverse the object step by step. + */ +// Your solution: +/** + * Function settings/config system that supports nested dot-notation access. + * @param {Object} defaults any object configuration. + * @returns {Object} createConfig(defaults) returns an object with: + * get(path) → reads a value at a dot-notation path + * set(path, value) → sets a value at a dot-notation path (mutates internal state) + * reset() → resets all config to the original defaults (deep clone) + * getAll() → returns the current config (a copy) + */ +const createConfig = (defaults) => { + let defaultsCopy = deepClone(defaults) + return { + /** + * Function that reads a value at a dot-notation path. + * @param {String} path any dot-notation path. + */ + get(path) { + // 1. split by "." + let pathSplit = path.split(".") + // 2. checks first element key in defaultsCopy + if (pathSplit[0] in defaultsCopy) { + // 3. checks key value isObject? + if (pathSplit.length === 1) { + // - primitive value + console.log(defaultsCopy[path]) + } else { + // 4. destructure arr route obj + let [key, ...route] = pathSplit + let result = defaultsCopy[key] + let isRoute = true + for (const keyPath of route) { + if (result[keyPath] !== undefined) { + result = result[keyPath] + } else { + isRoute = false + } + } + if (isRoute) { + console.log(result) + } + } + } + }, + /** + * Function that sets a value at a dot-notation path (mutates internal state) + * @param {String} path any dot-notation path. + * @param {String} value any new value. + */ + set(path, value) { + // 1. split by "." + let pathSplit = path.split(".") + // 2. checks first element key in defaultsCopy + if (pathSplit[0] in defaultsCopy) { + // 3. checks key value isObject? + if (pathSplit.length === 1) { + // - primitive value + defaultsCopy[path] = value + } else { + // 2. destructuring + let [pathOne, pathTwo] = pathSplit + if (defaultsCopy[pathOne][pathTwo] !== undefined) { + defaultsCopy[pathOne][pathTwo] = value + } + } + } + }, + /** + * Function that resets all config to the original defaults (deep clone). + */ + reset() { + defaultsCopy = deepClone(defaults) + }, + /** + * Function that returns the current config (a copy) + */ + getAll() { + return defaultsCopy + }, + } +} + +const config = createConfig({ + theme: { color: "blue", fontSize: 14 }, + language: "en", + features: { darkMode: false, notifications: true }, +}) + +config.get("theme.color") //→ "blue" +config.get("features.darkMode") //→ false +config.get("language") //→ "en" +config.get("width") //→ +config.set("theme.color", "red") +config.get("theme.color") //→ "red" +config.set("language", "es") +config.get("language") //→ "es" +config.reset() +config.get("theme.color") //→ "blue" (back to default) +config.get("language") //→ "en" (back to default) +// ── X3 (9 pts) ─────────────────────────── +console.log("\n<<<===>>> Combined Challenges 3 <<<===>>>\n") +/** + * Build a schema validator — a simplified version of what libraries like + * Zod or Yup do in React projects. + * + * createSchema(schema) returns a validate(data) function. + * Each schema field is an object with rules: + * { type, required, min, max, pattern, custom } + * + * validate(data) returns { valid: boolean, errors: { field: string[] } } + * Multiple errors per field are possible. + * + * const userSchema = createSchema({ + * name: { type: "string", required: true, min: 2, max: 50 }, + * age: { type: "number", required: true, min: 18, max: 120 }, + * email: { type: "string", required: true, pattern: /\S+@\S+\.\S+/ }, + * bio: { type: "string", required: false, max: 200 }, + * }) + * + * userSchema({ name:"Y", age:15, email:"notanemail" }) + * → { + * valid: false, + * errors: { + * name: ["Too short (min 2)"], + * age: ["Too small (min 18)"], + * email: ["Does not match pattern"], + * } + * } + * + * userSchema({ name:"Yoandy", age:25, email:"y@x.com" }) + * → { valid: true, errors: {} } + */ +// Your solution: +const createSchema = (schema) => { + /** + * createSchema(schema) returns a validate(data) function. + * Each schema field is an object with rules: + * { type, required, min, max, pattern, custom } + * + * validate(data) returns { valid: boolean, errors: { field: string[] } } + * Multiple errors per field are possible. + * + * const userSchema = createSchema({ + * name: { type: "string", required: true, min: 2, max: 50 }, + * age: { type: "number", required: true, min: 18, max: 120 }, + * email: { type: "string", required: true, pattern: /\S+@\S+\.\S+/ }, + * bio: { type: "string", required: false, max: 200 }, + * }) + * + * userSchema({ name:"Y", age:15, email:"notanemail" }) + * → { + * valid: false, + * errors: { + * name: ["Too short (min 2)"], + * age: ["Too small (min 18)"], + * email: ["Does not match pattern"], + * } + * } + */ + return function validate(data) { + const errors = Object.entries(schema).reduce((errs, rule) => { + const [keyRule, valueRule] = rule + if ((keyRule in data && valueRule.required === true) || (keyRule in data && valueRule.required === false)) { + const fieldErrs = Object.entries(valueRule) + .filter((schemaRule) => { + const [keyTest, valueTest] = schemaRule + switch (keyTest) { + case "type": + if (typeof data[keyRule] !== valueTest) { + return [keyTest, valueTest] + } + break + case "min": + if (typeof data[keyRule] === "string") { + if (data[keyRule].length < valueTest) { + return [keyTest, valueTest] + } + } else if (typeof data[keyRule] === "number") { + if (data[keyRule] < valueTest) { + return [keyTest, valueTest] + } + } + break + case "max": + if (typeof data[keyRule] === "string") { + if (data[keyRule].length > valueTest) { + return [keyTest, valueTest] + } + } else if (typeof data[keyRule] === "number") { + if (data[keyRule] > valueTest) { + return [keyTest, valueTest] + } + } + break + case "pattern": + let objRegEx = new RegExp(valueTest, "g") + if (objRegEx.exec(data[keyRule]) === null) { + return [keyTest, valueTest] + } + break + case "custom": + if (!valueTest(data[keyRule])) { + return [keyTest, valueTest] + } + break + default: + break + } + }) + .map((eachErr) => { + let [nameErr, valueErr] = eachErr + if (nameErr === "type") { + return `Diff type ${valueErr}` + } else if (nameErr === "min") { + return `Too short (min ${valueErr})` + } else if (nameErr === "max") { + return `Too small (min ${valueErr})` + } else if (nameErr === "pattern") { + return "Does not match pattern" + } else if (nameErr === "custom") { + return "Invalid custom" + } + }) + if (fieldErrs.length > 0) { + if (keyRule in errs) { + errs[keyRule].contact(...fieldErrs) + } else { + errs[keyRule] = fieldErrs + } + } + } else if (!Object.hasOwn(data, keyRule) && valueRule.required === true) { + errs[keyRule] = ["Element not found (required)"] + } + return errs + }, {}) + return { valid: Object.entries(errors).length > 0 ? false : true, errors: errors } + } +} + +const userSchema = createSchema({ + name: { + type: "string", + required: true, + min: 2, + max: 50, + custom(data) { + return data.length > 3 + }, + }, + age: { type: "number", required: true, min: 18, max: 120 }, + email: { type: "string", required: true, pattern: /\S+@\S+\.\S+/ }, + bio: { type: "string", required: false, max: 200 }, +}) + +console.log("VALIDATE ", userSchema({ name: "Y", age: 15, email: "notanemail" })) +console.log("VALIDATE 2 ", userSchema({ name: "Yoandy", age: 25, email: "y@x.com" })) + +// ════════════════════════════════════════ +// END OF EXAM +// ════════════════════════════════════════ +/* + Before submitting, verify: + [x] Every function has JSDoc + [x] Every function tested with at least 2 console.logs + [x] node fcc-ch5-objects-exam.js runs without errors + [x] No input objects were mutated (unless the exercise says to) + [x] Name and end time filled in at the top +*/ diff --git a/fullstack/javascript/5-objects/exam/objects-exam-grok-solution.js b/fullstack/javascript/5-objects/exam/objects-exam-grok-solution.js new file mode 100644 index 0000000..5f31235 --- /dev/null +++ b/fullstack/javascript/5-objects/exam/objects-exam-grok-solution.js @@ -0,0 +1,42 @@ +"use strict" +// ... (Vec and other parts unchanged - they were good) + +class Group { + constructor() { + this.members = [] + } + add(value) { + if (!this.has(value)) this.members.push(value) + } + delete(value) { + this.members = this.members.filter((v) => v !== value) + } + has(value) { + return this.members.includes(value) + } + + [Symbol.iterator]() { + let index = 0 + const members = this.members + return { + next: () => ({ + value: members[index], + done: index++ >= members.length, + }), + } + } +} + +// Tests Q4: +console.log("Q4 Tests:") +const group = new Group() +group.add(10) +group.add(20) +group.add(30) +console.log(">>>GROUP", group) + +const result = [] +for (let value of group) { + result.push(value) +} +console.log(result) // expected: [10, 20, 30] diff --git a/fullstack/javascript/5-objects/exam/objects-exam.js b/fullstack/javascript/5-objects/exam/objects-exam.js new file mode 100644 index 0000000..a0200e0 --- /dev/null +++ b/fullstack/javascript/5-objects/exam/objects-exam.js @@ -0,0 +1,183 @@ +/** + * ======================================== + * EXAM 2: THE SECRET LIFE OF OBJECTS + * ======================================== + * Focus: Eloquent JS Ch 6 (OOP, Prototypes, Classes, Polymorphism) + * Rules: Vanilla JS. + * ======================================== + */ +"use strict" +console.log("=== Part 1: Classes & Encapsulation ===") + +// ── Q1: The Vector Class ── +/** + * Write a class 'Vec' that represents a vector in two-dimensional space. + * It takes 'x' and 'y' parameters (numbers), which it should save to properties of the same name. + * * Give the Vec prototype two methods, 'plus' and 'minus', that take another + * vector as a parameter and return a NEW vector that has the sum or difference + * of the two vectors' (this and the parameter) x and y values. + * * Add a getter property 'length' to the prototype that computes the length of + * the vector (distance from the origin 0,0). Hint: Math.sqrt(x*x + y*y). + */ + +/** + * Class 'Vec' that represents a vector in two-dimensional space. + * It takes 'x' and 'y' parameters (numbers). + */ +class Vec { + /** + * A vector in two-dimensional space. + * @param {Number} x axis x. + * @param {Number} y axis y. + */ + constructor(x, y) { + this.x = x + this.y = y + } + /** + * Function that sum of the two vectors' (this and the parameter) x and y values. + * @param {Vec} vector any vector. + * @returns {Vec} a NEW vector. + */ + plus(vector) { + return new Vec(this.x + vector.x, this.y + vector.y) + } + + /** + * Function that returns the difference of the two vectors' (this and the parameter) x and y values. + * @param {Vec} vector any vector. + * @returns {Vec} a NEW vector. + */ + minus(vector) { + return new Vec(this.x - vector.x, this.y - vector.y) + } + + /** + * @returns {Number} the length of the vector (distance from the origin 0,0). + */ + get length() { + return Math.sqrt(this.x ** 2 + this.y ** 2) + } +} + +// Tests Q1: +console.log("Q1 Tests:") +console.log(new Vec(1, 2).plus(new Vec(2, 3))) // expected: Vec { x: 3, y: 5 } +console.log(new Vec(1, 2).minus(new Vec(2, 3))) // expected: Vec { x: -1, y: -1 } +console.log(new Vec(3, 4).length) // expected: 5 + +console.log("\n=== Part 2: Maps & Polymorphism ===") + +// ── Q2: Using Maps ── +/** + * + */ + +/** + * Create a function 'wordFrequency' that takes a string of text, splits it by spaces, and returns a standard JavaScript `Map` object where the keys are the unique words and the values are the number of times that word appeared. Ignore punctuation and capitalization for simplicity, just split by " ". + * @param {String} text any sentence. + * @returns {Map} returns a standard JavaScript `Map` object where the keys are the unique words and the values are the number of times that word appeared. + */ +function wordFrequency(text) { + // 1. lowerCase, remove punctuation, split by space. + const txtSplitted = text + .trim() + .toLowerCase() + .replace(/[^a-zA-Z0-9\s+]/g, "") + .split(/\s+/g) + return txtSplitted.reduce( + /** + * Reduce to create a word Map. + * @param {Map} objMap acc Obj. + * @param {String} word each word. + */ + (objMap, word) => { + if (objMap.has(word)) { + return objMap.set(word, objMap.get(word) + 1) + } else { + return objMap.set(word, 1) + } + }, + new Map(), + ) +} + +// Tests Q2: +console.log("Q2 Tests:") +const freqMap = wordFrequency("the quick brown fox jumps over the lazy dog") +console.log(freqMap.get("the")) // expected: 2 +console.log(freqMap.has("fox")) // expected: true + +// ── Q3: Polymorphism (Overriding) ── +/** + * 1. Create a class 'Temperature' that takes Celsius in its constructor. + * 2. Override the standard `toString` method so that when you try to print + * the object as a string, it returns "X°C" (where X is the temperature). + */ +class Temperature { + /** + * @param {Number} celsius any number celsius. + */ + constructor(celsius) { + this.celsius = celsius + } + toString() { + return `${this.celsius}°C` + } +} +// Tests Q3: +console.log("\nQ3 Tests:") +const temp = new Temperature(22) +console.log(String(temp)) // expected: "22°C" + +console.log("\n=== Part 3: Iterators & Symbols (Advanced) ===") + +// ── Q4: The Iterable Group ── +/** + * Write a class called 'Group' (like a Set). + * It has 'add', 'delete', and 'has' methods. + * Its constructor creates an empty array to store members. + * * Challenge: Make the Group class iterable. Add a Symbol.iterator method + * so that you can use a 'for...of' loop directly on a Group instance. + */ + +class Group { + index = 0 + constructor() { + this.members = [] + } + add(value) { + if (!this.has(value)) this.members.push(value) + } + delete(value) { + this.members = this.members.filter((v) => v !== value) + } + has(value) { + return this.members.includes(value) + } + + // Implement Symbol.iterator here + [Symbol.iterator]() { + return function next() { + if (this.members.length === 0) { + return { done: true } + } else if (this.index <= this.members.length - 1) { + return { value: this.members[this.index], done: false } + } + } + } +} + +// Tests Q4: +console.log("Q4 Tests:") +const group = new Group() +group.add(10) +group.add(20) +group.add(30) +console.log(">>>GROUP", group) + +const result = [] +for (let value of group) { + result.push(value) +} +console.log(result) // expected: [10, 20, 30] diff --git a/fullstack/javascript/5-objects/exam/objects-loops-exam.js b/fullstack/javascript/5-objects/exam/objects-loops-exam.js new file mode 100644 index 0000000..864b62c --- /dev/null +++ b/fullstack/javascript/5-objects/exam/objects-loops-exam.js @@ -0,0 +1,171 @@ +/** + * ======================================== + * EXAM 1: OBJECTS & LOOPS FOUNDATIONS + * ======================================== + * Focus: freeCodeCamp Objects & Loops + * Rules: Vanilla JS only. No looking at the docs. + * Run the file to see if your tests pass. + * ======================================== + */ +"use strict" +console.log("=== Part 1: Nested Data & Iteration ===") + +// ── Q1: Record Collection (A classic FCC concept) ── +console.log("\n<<<===>>> Question 1 <<<===>>>\n") +const recordCollection = { + 2548: { albumTitle: "Slippery When Wet", artist: "Bon Jovi", tracks: ["Let It Rock", "You Give Love a Bad Name"] }, + 2468: { albumTitle: "1999", artist: "Prince", tracks: ["1999", "Little Red Corvette"] }, + 1245: { artist: "Robert Palmer", tracks: [] }, + 5439: { albumTitle: "ABBA Gold" }, +} + +/** + * Write a function that takes an ID, a property (prop), and a value. + * - If value is empty string, delete the given prop. + * - If prop isn't "tracks" and value isn't empty, assign value to that prop. + * - If prop is "tracks" and value isn't empty, push the value to the tracks array + * (create the array first if it doesn't exist). + * Return the entire collection object. + * @param {Object} records Object record collection. + * @param {Number} id any id in record collection. + * @param {String} prop any prop to update/delete in record. + * @param {String} value any value to assign in record. If empty delete prop. + * @returns {Object} Object record collection. + */ +function updateRecords(records, id, prop, value) { + // 1. checks value === "" delete prop + if (!value.trim()) { + if (prop in records[id]) { + delete records[id][prop] + } + return records + } + if (prop !== "tracks") { + //2. prop diff tracks, assign value to that prop. + records[id][prop] = value + } else { + //3. If prop is "tracks" and value isn't empty, push the value to the tracks + if (!Object.hasOwn(records[id], prop)) { + records[id][prop] = [] + } + records[id][prop].push(value) + } + return records +} + +// Tests Q1: +console.log("Q1 Tests:") +updateRecords(recordCollection, 5439, "artist", "ABBA") +console.log(recordCollection[5439].artist === "ABBA") // expected: true +updateRecords(recordCollection, 5439, "tracks", "Take a Chance on Me") +console.log(recordCollection[5439].tracks[0] === "Take a Chance on Me") // expected: true + +// ── Q2: Deep Object Iteration (for...in) ── +console.log("\n<<<===>>> Question 2 <<<===>>>\n") +/** + * Write a function that takes an object and returns the sum of all numeric + * properties. If a property is an object itself, use recursion to sum its numbers too. + */ +const mixedData = { a: 1.55, b: "hello", c: { d: 10, e: { f: 5 } }, g: 20.32, h: ["Array", 2, 65.3, "8"], i: null, x: 36 } + +/** + * Function that takes an object and returns the sum of all numeric + * properties. If a property is an object itself, use recursion to sum its numbers too. + * @param {Object} obj any object. + * @returns {Number} the sum of all numeric properties. + */ +function sumDeepNumbers(obj) { + // 1. use reduce + return Object.entries(obj).reduce((sum, prop) => { + const [, value] = prop + // 1. checks value is number + if (typeof value === "number") { + sum += value + } + // 2. checks array + if (Array.isArray(value)) { + sum += sumDeepNumbers(value) + } + // 3. checks obj diff null + if (typeof value === "object" && value !== null) { + sum = sum + sumDeepNumbers(value) + } + return sum + }, 0) +} + +// Tests Q2: +console.log("\nQ2 Tests:") +console.log(sumDeepNumbers(mixedData)) // expected: 40 + +console.log("\n=== Part 2: Loops & Recursion ===") + +// ── Q3: Multi-dimensional Array Looping ── +console.log("\n<<<===>>> Question 3 <<<===>>>\n") +/** + * Write a function that takes a 2D array and returns a 1D array of the largest + * number from each sub-array. Use nested 'for' or 'while' loops (no .map or .reduce). + */ +/** + * Function that takes a 2D array and returns a 1D array of the largest number from each sub-array. + * @param {Array} arr any 2D array of numbers. + * @returns {Array} a 1D array of the largest number from each sub-array. + */ +function largestOfFour(arr) { + return arr.reduce( + /** + * Reduce to find largest numbers in 2D array. + * @param {Array} largest final acc of largest numbers. + * @param {Array} arrOneD any array. + * @returns {Array} array of largest numbers. + */ + (largest, arrOneD) => { + let largeValue = -Infinity + for (const element of arrOneD) { + if (element > largeValue) largeValue = element + } + largest.push(largeValue) + return largest + }, + [], + ) +} + +// Tests Q3: +console.log("Q3 Tests:") +console.log( + largestOfFour([ + [4, 5, 1, 3], + [13, 27, 18, 26], + [32, 35, 37, 39], + [1000, 1001, 857, 1], + ]), +) +// expected: [5, 27, 39, 1001] + +// ── Q4: Pure Recursion ── +console.log("\n<<<===>>> Question 4 <<<===>>>\n") +/** + * Write a recursive function that returns an array containing the numbers `startNum` through `endNum`. + * Do NOT use loops. + */ + +/** + * Recursive function that returns an array containing the numbers `startNum` through `endNum`. + * @param {Number} startNum any number smaller than endNum. + * @param {Number} endNum any number bigger than startNum. + * @returns {Array} an array containing the numbers `startNum` through `endNum`. + */ +function rangeOfNumbers(startNum, endNum) { + // 1. checks startNum === endNum + if (startNum === endNum) { + return [endNum] + } + // 2. recursion with acc + return [startNum, ...rangeOfNumbers(startNum + 1, endNum)] +} + +// Tests Q4: +console.log("\nQ4 Tests:") +console.log(rangeOfNumbers(1, 5)) // expected: [1, 2, 3, 4, 5] +console.log(rangeOfNumbers(4, 4)) // expected: [4] diff --git a/fullstack/javascript/5-objects/fcc-ch5-objects-review.js b/fullstack/javascript/5-objects/fcc-ch5-objects-review.js new file mode 100644 index 0000000..60f8c1a --- /dev/null +++ b/fullstack/javascript/5-objects/fcc-ch5-objects-review.js @@ -0,0 +1,346 @@ +/** + * freeCodeCamp — Chapter 5: Objects + * Exercises reviewed and fixed + * Author: Yoandy Doble Herrera + * Reviewed by: Claude + * + * EXERCISES: + * 1. Cargo Manifest Validator — 26/30 (3 bugs fixed) + * 2. Quiz Game — 30/30 (perfect) + * 3. Record Collection — 28/30 (1 minor cleanup) + * + * REACT CONNECTION (read this): + * These three exercises are React patterns in disguise: + * normalizeUnits → immutable state updates (spread + return new) + * validateManifest → form validation (errors object pattern) + * updateRecords → useReducer (state + action → new state) + */ + +// ═══════════════════════════════════════════════ +// EXERCISE 1 — Cargo Manifest Validator +// ═══════════════════════════════════════════════ + +// --- Test data --- +const manifest = { + containerId: 1, + destination: "Monterey, California, USA", + weight: 831, + unit: "lb", + hazmat: false, +} +const validManifest = { + containerId: 2, + destination: "Queretaro, Queretaro, MX", + weight: 1000, + unit: "kg", + hazmat: true, +} +const invalidManifest = { + containerId: 3, // missing: destination; invalid: weight (string) + weight: "500", + unit: "kg", + hazmat: true, +} + +// ── normalizeUnits (10/10 ✓) ────────────────── +/** + * Converts a manifest's weight to kilograms. + * Never mutates the original — always returns a new object. + * @param {Object} manifest cargo manifest object. + * @returns {Object} new manifest with weight in kg. + */ +const normalizeUnits = (manifest) => { + // Spread creates a shallow copy — original is safe + let normalizedManifest = { ...manifest } + if (normalizedManifest.unit === "lb") { + normalizedManifest.weight = normalizedManifest.weight * 0.45 + normalizedManifest.unit = "kg" + } + return normalizedManifest +} + +console.log("=== Exercise 1: normalizeUnits ===") +console.log(normalizeUnits(manifest)) // weight: 373.95, unit: "kg" +console.log(normalizeUnits(validManifest)) // unchanged (already kg) +console.log("Original untouched:", manifest.unit) // "lb" — immutable ✓ + +// ── validateManifest (8/10 → Fixed) ────────── +/** + * Validates all required manifest properties. + * Returns {} if valid, or { field: "Missing"|"Invalid" } for each problem. + * + * BUGS FIXED: + * 1. containerId: < 0 → <= 0 (spec: positive integer, 0 is not positive) + * 2. containerId: added Number.isInteger() (1.5 is not a valid integer ID) + * + * @param {Object} manifest cargo manifest object. + * @returns {Object} empty if valid, error map if not. + */ +const validateManifest = (manifest) => { + const requiredKeys = ["containerId", "destination", "weight", "unit", "hazmat"] + let errors = {} + + for (const key of requiredKeys) { + if (!(key in manifest)) { + errors[key] = "Missing" + continue + } + + switch (key) { + case "containerId": + // FIX: was (< 0) — 0 is not a positive integer + if ( + typeof manifest[key] !== "number" || + !Number.isInteger(manifest[key]) || // must be integer, not float + manifest[key] <= 0 // must be POSITIVE (> 0) + ) { + errors[key] = "Invalid" + } + break + + case "destination": + if ( + typeof manifest[key] !== "string" || + manifest[key].trim() === "" // trim() is cleaner than replacing all whitespace + ) { + errors[key] = "Invalid" + } + break + + case "weight": + if (typeof manifest[key] !== "number" || manifest[key] <= 0) { + errors[key] = "Invalid" + } + break + + case "unit": + if ( + typeof manifest[key] !== "string" || + (manifest[key] !== "kg" && manifest[key] !== "lb") + ) { + errors[key] = "Invalid" + } + break + + case "hazmat": + if (typeof manifest[key] !== "boolean") { + errors[key] = "Invalid" + } + break + } + } + return errors +} + +console.log("\n=== Exercise 2: validateManifest ===") +console.log(validateManifest(manifest)) // {} — valid +console.log(validateManifest(validManifest)) // {} — valid +console.log(validateManifest(invalidManifest)) // { destination: "Missing", weight: "Invalid" } +console.log(validateManifest({ ...manifest, containerId: 0 })) // { containerId: "Invalid" } ✓ fixed +console.log(validateManifest({ ...manifest, containerId: 1.5 })) // { containerId: "Invalid" } ✓ fixed + +// ── processManifest (8/10 → Fixed) ─────────── +/** + * Processes a manifest: validates it and logs the result. + * + * BUGS FIXED: + * 1. Spec requires TWO separate console.log() calls per branch + * 2. ${errors} in template literal prints "[object Object]" + * Fix: log the object directly in the second console.log() + * 3. validateManifest() was called 3 times — now called once and stored + * + * @param {Object} manifest cargo manifest object. + * @returns {String} validation result message. + */ +const processManifest = (manifest) => { + const errors = validateManifest(manifest) // call ONCE, store result + const isValid = Object.keys(errors).length === 0 + + if (isValid) { + const normalized = normalizeUnits(manifest) + console.log(`Validation success: ${manifest.containerId}`) // log 1 + console.log(`Total weight: ${normalized.weight} kg`) // log 2 ← spec requires this separately + return `Validation success: ${manifest.containerId} Total weight: ${normalized.weight} kg` + } else { + console.log(`Validation error: ${manifest.containerId}`) // log 1 + console.log(errors) // log 2 ← logs the object properly (not [object Object]) + return `Validation error: ${manifest.containerId}` + } +} + +console.log("\n=== Exercise 3: processManifest ===") +processManifest(manifest) // success +processManifest(validManifest) // success +processManifest(invalidManifest) // error + + +// ═══════════════════════════════════════════════ +// EXERCISE 2 — Quiz Game (30/30 ✓ — perfect) +// ═══════════════════════════════════════════════ + +const questions = [ + { + category: "Science", + question: "What is the chemical symbol for water?", + choices: ["H2O", "CO2", "O2"], + answer: "H2O", + }, + { + category: "Geography", + question: "Which is the largest ocean on Earth?", + choices: ["Atlantic Ocean", "Pacific Ocean", "Indian Ocean"], + answer: "Pacific Ocean", + }, + { + category: "Technology", + question: "What does CPU stand for?", + choices: ["Central Process Unit", "Central Processing Unit", "Computer Personal Unit"], + answer: "Central Processing Unit", + }, + { + category: "History", + question: "In which year did World War II end?", + choices: ["1945", "1939", "1918"], + answer: "1945", + }, + { + category: "Space", + question: "Which planet is known as the Red Planet?", + choices: ["Venus", "Mars", "Jupiter"], + answer: "Mars", + }, +] + +/** + * Returns a random question from the array. + * @param {Array} questions array of question objects. + * @returns {Object} random question object. + */ +const getRandomQuestion = (questions) => + questions[Math.floor(Math.random() * questions.length)] + +/** + * Returns a random choice from the choices array. + * @param {Array} choices array of choice strings. + * @returns {String} random choice. + */ +const getRandomComputerChoice = (choices) => + choices[Math.floor(Math.random() * choices.length)] + +/** + * Evaluates whether the computer's choice is correct. + * @param {Object} question question object with answer property. + * @param {String} computerChoice the computer's selected answer. + * @returns {String} result message. + */ +const getResults = (question, computerChoice) => + question.answer === computerChoice + ? `The computer's choice is correct!` + : `The computer's choice is wrong. The correct answer is: ${question.answer}` + +console.log("\n=== Quiz Game ===") +const randomQuestion = getRandomQuestion(questions) +const randomAnswer = getRandomComputerChoice(randomQuestion.choices) +console.log("Question:", randomQuestion.question) +console.log("Computer chose:", randomAnswer) +console.log(getResults(randomQuestion, randomAnswer)) + +// Verify all answers are in their choices (data integrity check) +const allValid = questions.every((q) => q.choices.includes(q.answer)) +console.log("All answers in choices:", allValid) // true ✓ + + +// ═══════════════════════════════════════════════ +// EXERCISE 3 — Record Collection (28/30 → Cleaned) +// ═══════════════════════════════════════════════ + +const recordCollection = { + 2548: { + albumTitle: "Slippery When Wet", + artist: "Bon Jovi", + tracks: ["Let It Rock", "You Give Love a Bad Name"], + }, + 2468: { + albumTitle: "1999", + artist: "Prince", + tracks: ["1999", "Little Red Corvette"], + }, + 1245: { + artist: "Robert Palmer", + tracks: [], + }, + 5439: { + albumTitle: "ABBA Gold", + }, +} + +/** + * Updates a record in the collection based on the given prop and value. + * Rules: + * - Empty value → delete the prop + * - Non-tracks prop + value → set prop to value + * - tracks prop + value, no tracks array → create array and push value + * - tracks prop + value, tracks exists → push value + * + * CLEANUP: removed internal console.logs (spec only requires returning records) + * CLEANUP: removed redundant 'prop in records[id]' check before delete + * (delete on non-existent key is silently ignored in JS) + * + * @param {Object} records the full record collection. + * @param {Number} id album ID. + * @param {String} prop property name to update. + * @param {String} value new value ("" to delete). + * @returns {Object} the updated records object. + */ +const updateRecords = (records, id, prop, value) => { + if (!value) { + // Empty string → delete property (delete on missing key is a no-op) + delete records[id][prop] + } else if (prop !== "tracks") { + // Any non-tracks prop → assign directly + records[id][prop] = value + } else { + // tracks prop → create if missing, then push + if (!records[id][prop]) records[id][prop] = [] + records[id][prop].push(value) + } + return records +} + +console.log("\n=== Record Collection ===") + +// US2: delete prop with empty string +console.log(updateRecords(recordCollection, 1245, "artist", "")) +// US3: add non-tracks prop +console.log(updateRecords(recordCollection, 5439, "year", "1988")) +// US4+5: add track (creates array since 5439 has no tracks) +console.log(updateRecords(recordCollection, 5439, "tracks", "ABBA Ultimate")) +// US5: append to existing tracks +console.log(updateRecords(recordCollection, 2548, "tracks", "New Song")) + + +// ═══════════════════════════════════════════════ +// REACT CONNECTION — why these exercises matter +// ═══════════════════════════════════════════════ +/* + normalizeUnits pattern → immutable state update in React: + const [state, setState] = useState(initialManifest) + const handleNormalize = () => setState(normalizeUnits(state)) + // spread + return new = the ONLY safe way to update React state + + validateManifest pattern → form validation in React: + const [errors, setErrors] = useState({}) + const handleSubmit = () => { + const errs = validateManifest(formData) + setErrors(errs) + if (Object.keys(errs).length === 0) submitForm() + } + + updateRecords pattern → useReducer in React: + const reducer = (state, action) => { + // action.type, action.id, action.prop, action.value + return updateRecords({ ...state }, action.id, action.prop, action.value) + } + const [records, dispatch] = useReducer(reducer, recordCollection) + // This is exactly how Redux works +*/ diff --git a/fullstack/javascript/5-objects/quiz-game.js b/fullstack/javascript/5-objects/quiz-game.js new file mode 100644 index 0000000..900d208 --- /dev/null +++ b/fullstack/javascript/5-objects/quiz-game.js @@ -0,0 +1,79 @@ +const questions = [ + { + category: "Science", + question: "What is the chemical symbol for water?", + choices: ["H2O", "CO2", "O2"], + answer: "H2O", + }, + { + category: "Geography", + question: "Which is the largest ocean on Earth?", + choices: ["Atlantic Ocean", "Pacific Ocean", "Indian Ocean"], + answer: "Pacific Ocean", + }, + { + category: "Technology", + question: "What does CPU stand for?", + choices: ["Central Process Unit", "Central Processing Unit", "Computer Personal Unit"], + answer: "Central Processing Unit", + }, + { + category: "History", + question: "In which year did World War II end?", + choices: ["1945", "1939", "1918"], + answer: "1945", + }, + { + category: "Space", + question: "Which planet is known as the Red Planet?", + choices: ["Venus", "Mars", "Jupiter"], + answer: "Mars", + }, +] + +/** + * Function that takes an array of questions as a parameter and returns a random question object from the array. + * @param {Array} questions Any array of questions. + * @returns {Object} A random question object from the array. + */ +const getRandomQuestion = (questions) => { + const randomNumber = Math.floor(Math.random() * questions.length) + return questions[randomNumber] +} + +/** + * Function that takes the array of the available choices as a parameter, and returns a random answer to the selected question. + * @param {Array} choices Any array of the available choices to the selected question. + * @returns {String} A random answer to the selected question. + */ +const getRandomComputerChoice = (choices) => { + const randomNumber = Math.floor(Math.random() * choices.length) + return choices[randomNumber] +} + +/** + * Function that takes the question object as the first parameter and the computer's choice as the second parameter. The function should return The computer's choice is correct! if the answer is correct. Otherwise, it returns The computer's choice is wrong. The correct answer is: **correct-answer**, where **correct-answer** is the value of the correct answer to the chosen question. + * @param {Object} question Any question object. + * @param {String} computerChoice Computer's choice: a random answer to the selected question. + * @returns {String} The computer's choice is correct! if the answer is correct. Otherwise, it returns The computer's choice is wrong. The correct answer is: **correct-answer**, where **correct-answer** is the value of the correct answer to the chosen question. + */ +const getResults = (question, computerChoice) => { + if (question.answer === computerChoice) { + return `The computer's choice is correct!` + } else { + return `The computer's choice is wrong. The correct answer is: ${question.answer}` + } +} + +// --- Test Cases --- +console.log("--- QUIZ GAME ---") + +const randomQuestion = getRandomQuestion(questions) +console.log(">>>Random Question", randomQuestion) + +const randomAnswer = getRandomComputerChoice(randomQuestion.choices) +console.log(">>>Random Answer", randomAnswer) + +console.log(getResults(randomQuestion, randomAnswer)) + +console.log("\n====================================") diff --git a/fullstack/javascript/5-objects/quiz-game.md b/fullstack/javascript/5-objects/quiz-game.md new file mode 100644 index 0000000..30ff2ba --- /dev/null +++ b/fullstack/javascript/5-objects/quiz-game.md @@ -0,0 +1,15 @@ +# Build a Quiz Game + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create an array named questions. +2. [x] The questions array should contain at least five objects, each having the keys category, question, choices, and answer. +3. [x] The category key should have the value of a string representing a question category. +4. [x] The question key should have the value of a string representing a question. +5. [x] The choices key should have the value of an array containing three strings, which are alternative answers to the question. +6. [x] The answer key should have the value of a string, representing the correct answer to the question. Also, the value of answer should be included in the choices array. +7. [x] You should have a function named getRandomQuestion that takes an array of questions as a parameter and returns a random question object from the array. +8. [x] You should have a function named getRandomComputerChoice that takes the array of the available choices as a parameter, and returns a random answer to the selected question. +9. [x] You should have a function named getResults that takes the question object as the first parameter and the computer's choice as the second parameter. The function should return The computer's choice is correct! if the answer is correct. Otherwise, it returns The computer's choice is wrong. The correct answer is: **correct-answer**, where **correct-answer** is the value of the correct answer to the chosen question. diff --git a/fullstack/javascript/5-objects/record-collection.js b/fullstack/javascript/5-objects/record-collection.js new file mode 100644 index 0000000..76e6832 --- /dev/null +++ b/fullstack/javascript/5-objects/record-collection.js @@ -0,0 +1,49 @@ +const recordCollection = { + 2548: { + albumTitle: "Slippery When Wet", + artist: "Bon Jovi", + tracks: ["Let It Rock", "You Give Love a Bad Name"], + }, + 2468: { + albumTitle: "1999", + artist: "Prince", + tracks: ["1999", "Little Red Corvette"], + }, + 1245: { + artist: "Robert Palmer", + tracks: [], + }, + 5439: { + albumTitle: "ABBA Gold", + }, +} + +/** + * + * @param {Object} records an object containing several individual albums. + * @param {Number} id a number representing a specific album in the records object. + * @param {String} prop a string representing the name of the album’s property to update. + * @param {String} value a string containing the information used to update the album’s property + */ +const updateRecords = (records, id, prop, value) => { + // 1. If value is an empty string, delete the given prop property from the album. + if (!value) { + if (prop in records[id]) { + delete records[id][prop] + console.log("Deleted prop: " + prop + " from album " + id + "\n") + } else { + console.log("Property not found on album " + id + "\n") + } + } else if (prop !== "tracks") { + records[id][prop] = value + } else { + if (!records[id][prop]) { + records[id][prop] = [] + } + records[id][prop].push(value) + } + return records +} +console.log(updateRecords(recordCollection, 1245, "artist", "")) +console.log(updateRecords(recordCollection, 5439, "year", "1988")) +console.log(updateRecords(recordCollection, 5439, "tracks", "ABBA Ultimate")) diff --git a/fullstack/javascript/5-objects/record-collection.md b/fullstack/javascript/5-objects/record-collection.md new file mode 100644 index 0000000..e12ff45 --- /dev/null +++ b/fullstack/javascript/5-objects/record-collection.md @@ -0,0 +1,21 @@ +# Build a Record Collection + +You are creating a function that aids in the maintenance of a musical album collection. The collection is organized as an object that contains multiple albums which are also objects. Each album is represented in the collection with a unique id as the property name. Within each album object, there are various properties describing information about the album. Not all albums have complete information. +The updateRecords function takes 4 arguments represented by the following function parameters: + +- records - an object containing several individual albums +- id - a number representing a specific album in the records object +- prop - a string representing the name of the album’s property to update +- value - a string containing the information used to update the album’s property + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Your function must always return the entire records object. +2. [x] If value is an empty string, delete the given prop property from the album. +3. [x] If prop isn't tracks and value isn't an empty string, assign the value to that album's prop. +4. [x] If prop is tracks and value isn't an empty string, but the album doesn't have a tracks property, create an empty array and add value to it. +5. [x] If prop is tracks and value isn't an empty string, add value to the end of the album's existing tracks array. + +**Note**: A copy of the recordCollection object is used for the tests. Your function should not directly refer to the recordCollection object, only the function parameter. diff --git a/fullstack/javascript/6-loops/ch6-review-gemini.js b/fullstack/javascript/6-loops/ch6-review-gemini.js new file mode 100644 index 0000000..8378040 --- /dev/null +++ b/fullstack/javascript/6-loops/ch6-review-gemini.js @@ -0,0 +1,45 @@ +/** + * ======================================================== + * FCC ALGORITHM REFRESHER (OPTIMIZED) + * ======================================================== + */ + +// 1. Longest Word (Returns Number) +const findLongestWordLength = (str) => { + return Math.max(...str.split(" ").map(word => word.length)); +}; + +// 2. Missing Letter (ASCII Comparison) +const fearNotLetter = (str) => { + for (let i = 0; i < str.length - 1; i++) { + if (str.charCodeAt(i + 1) - str.charCodeAt(i) > 1) { + return String.fromCharCode(str.charCodeAt(i) + 1); + } + } +}; + +// 3. Mutations (Boolean existence check) +const mutation = (arr) => { + const target = arr[0].toLowerCase(); + const test = arr[1].toLowerCase(); + // Check if every char in test exists in target + return test.split("").every(char => target.includes(char)); +}; + +// 4. Profile Lookup (Find first match) +const lookUpProfile = (name, prop) => { + const contact = contacts.find(c => c.name === name); + if (!contact) return "No such contact"; + return contact.hasOwnProperty(prop) ? contact[prop] : "No such property"; +}; + +// 5. Repeat String (Concatenation) +const repeatStringNumTimes = (str, num) => { + let result = ""; + for (let i = 0; i < num; i++) result += str; + return result; +}; + +// --- Visualization of logic --- +// +// \ No newline at end of file diff --git a/fullstack/javascript/6-loops/chunky-monkey-algorithm.js b/fullstack/javascript/6-loops/chunky-monkey-algorithm.js new file mode 100644 index 0000000..cc61f19 --- /dev/null +++ b/fullstack/javascript/6-loops/chunky-monkey-algorithm.js @@ -0,0 +1,31 @@ +/** + * Function named chunkArrayInGroups that takes an array as first argument and a number as second argument. The function should split the array into smaller arrays of length equal to the second argument and returns them as a two-dimensional array. + * @param {Array} arr any array. + * @param {Number} num length to split array. + * @returns {Array} a two-dimensional array. + */ +const chunkArrayInGroups = (arr, num) => { + let chunky = [] + let numArr = [] + if (num >= arr.length) return [].concat(arr) + for (let index = 0; index < arr.length; index++) { + if (numArr.length < num) { + numArr.push(arr[index]) + } + if (index === arr.length - 1) { + chunky.push(numArr) + return chunky + } + if (numArr.length === num) { + chunky.push(numArr) + numArr = [] + } + } + return chunky +} + +// --- Test Cases --- +console.log("--- Chunky Monkey Algorithm ---") +console.log(chunkArrayInGroups([1, 2, 3, 4, 5, 6, "Yoandy", 8, true, 10], 3)) +console.log(chunkArrayInGroups([1, 2, 3, 4, 5, 6, "Yoandy", 8, true, 10], 1)) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/chunky-monkey-algorithm.md b/fullstack/javascript/6-loops/chunky-monkey-algorithm.md new file mode 100644 index 0000000..fa5dce9 --- /dev/null +++ b/fullstack/javascript/6-loops/chunky-monkey-algorithm.md @@ -0,0 +1,7 @@ +# Implement the Chunky Monkey Algorithm + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Write a function named chunkArrayInGroups that takes an array as first argument and a number as second argument. The function should split the array into smaller arrays of length equal to the second argument and returns them as a two-dimensional array. diff --git a/fullstack/javascript/6-loops/factorial-calculator.js b/fullstack/javascript/6-loops/factorial-calculator.js new file mode 100644 index 0000000..bb0a72e --- /dev/null +++ b/fullstack/javascript/6-loops/factorial-calculator.js @@ -0,0 +1,20 @@ +const num = 8 + +/** + * Function that calculates factorial. + * @param {Number} num any number. + * @returns {Number} the factorial of that number. + */ +const factorialCalculator = (num) => { + let result = 1 + for (let index = 1; index <= num; index++) { + result *= index + } + return result +} +// --- Test Cases --- +console.log("--- Factorial Calculator ---") +let factorial = factorialCalculator(num) +const resultMsg = `Factorial of ${num} is ${factorial}` +console.log(resultMsg) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/factorial-calculator.md b/fullstack/javascript/6-loops/factorial-calculator.md new file mode 100644 index 0000000..cf6326a --- /dev/null +++ b/fullstack/javascript/6-loops/factorial-calculator.md @@ -0,0 +1,14 @@ +# Build a Factorial Calculator + +A factorial is the product of an integer and all the integers below it. For example, the factorial of 5 is 5 -> 4 -> 3 -> 2 -> 1 = 120. In this lab, you will create a factorial calculator that takes a number from the user and calculates the factorial of that number. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should declare a variable num and assign it a number of your choice. The assigned number should be between 1 and 20 inclusive. +2. [x] Create a function named factorialCalculator that takes a number as an argument and returns the factorial of that number. +3. [x] Inside the function, declare a result variable and assign it the value of 1. Using a loop, loop through all numbers from 1 to the input number(inclusive) and for each number, multiply the result variable by the current number and assign the result to the result variable. You can choose to use either a for loop, while loop or do...while loop here. +4. [x] You should call the factorialCalculator function with num as the argument and assign the result to the variable factorial. +5. [x] You should store the final output in the format Factorial of [num] is [factorial] and assign it to the variable resultMsg. +6. [x] You should output the value of resultMsg to the console. diff --git a/fullstack/javascript/6-loops/longest-word-finder-app.js b/fullstack/javascript/6-loops/longest-word-finder-app.js new file mode 100644 index 0000000..ddc6e29 --- /dev/null +++ b/fullstack/javascript/6-loops/longest-word-finder-app.js @@ -0,0 +1,12 @@ +/** + * Function that returns the length of the longest word in the provided sentence. + * @param {String} str any sentence. + * @returns {Number} length of the longest word in the provided sentence. + */ +const findLongestWordLength = (str) => { + const strArr = str.replace(/[^A-Za-z\s+]/g, "").split(/\s+/) + return strArr.sort((a, b) => b.length - a.length)[0] +} + +const longestWordLength = findLongestWordLength("The quick, brown fox. Jumped over the lazy dog") +console.log(longestWordLength) diff --git a/fullstack/javascript/6-loops/longest-word-finder-app.md b/fullstack/javascript/6-loops/longest-word-finder-app.md new file mode 100644 index 0000000..70ec95d --- /dev/null +++ b/fullstack/javascript/6-loops/longest-word-finder-app.md @@ -0,0 +1,11 @@ +# Build a Longest Word Finder App + +In this lab, you will build a function that returns the length of the longest word in the provided sentence. +For example, in the sentence "The quick, brown fox. Jumped over the lazy dog", the longest word is "jumped", which has a length of 6. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named findLongestWordLength that takes a string as an argument. +2. [x] The function should return the length of the longest word in the string. diff --git a/fullstack/javascript/6-loops/missing-letter-detector-.js b/fullstack/javascript/6-loops/missing-letter-detector-.js new file mode 100644 index 0000000..35985e9 --- /dev/null +++ b/fullstack/javascript/6-loops/missing-letter-detector-.js @@ -0,0 +1,54 @@ +/** + * Function that find the missing letter in the passed letter range and return it. + * @param {String} str a string representing a range of letters in alphabetical order which can have one letter missing. + * @returns {String|undefined} missing letter or undefined if all letters are present in the range. + */ +const fearNotLetter = (str) => { + const lettersDicc = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + ] + let missing = [] + let idx = lettersDicc.findIndex((letter) => letter === str[0].toLowerCase()) + if (str.length === 0) return "Empty string." + if (idx === -1) return "The elements isn't a letter. " + + for (let index = 0; index < str.length; index++) { + while (str[index].toLowerCase() !== lettersDicc[idx]) { + // add missing letters + missing.push(lettersDicc[idx]) + idx++ + } + idx++ + } + return missing.join(",") || undefined +} + +// --- Test Cases --- +console.log("--- Missing Letter Detector- ---") +console.log(fearNotLetter("MnPQrSU")) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/missing-letter-detector.md b/fullstack/javascript/6-loops/missing-letter-detector.md new file mode 100644 index 0000000..19b8196 --- /dev/null +++ b/fullstack/javascript/6-loops/missing-letter-detector.md @@ -0,0 +1,10 @@ +# Build a Missing Letter Detector + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named fearNotLetter. +2. [x] The fearNotLetter function should accept one argument: a string representing a range of letters in alphabetical order which can have one letter missing. +3. [x] The function should find the missing letter in the passed letter range and return it. +4. [x] If all letters are present in the range, the function should return undefined. diff --git a/fullstack/javascript/6-loops/mutations-algorithm.js b/fullstack/javascript/6-loops/mutations-algorithm.js new file mode 100644 index 0000000..842362e --- /dev/null +++ b/fullstack/javascript/6-loops/mutations-algorithm.js @@ -0,0 +1,42 @@ +/** + * Function that return true if the string in the first element of the array contains all of the letters of the string in the second element of the array, and false otherwise. + * @param {Array} arr any array with 2 elements. + * @returns {Boolean} return true if the string in the first element of the array contains all of the letters of the string in the second element of the array, and false otherwise. + */ +const mutation = (arr) => { + const [firstWord, secondWord] = arr + const firstWordMapped = lettersMap(firstWord) + const secondWordMapped = lettersMap(secondWord) + // 1. Compare occurrences each map + for (const [key, value] of secondWordMapped.entries()) { + if (!firstWordMapped.has(key)) { + return false + } else if (firstWordMapped.get(key) !== value) { + return false + } + } + return true +} + +/** + * Function that groupBy letter occurrences. + * @param {String} str any word. + * @returns {Map} groupBy letter occurrences. + */ +const lettersMap = (str) => { + let strMapped = new Map() + const strArr = str.toLowerCase().split("") + strArr.forEach((letter) => { + strMapped.set(letter, (strMapped.get(letter) || 0) + 1) + }) + return strMapped +} +// --- Test Cases --- +console.log("--- Mutations Algorithm ---") +const checkWords = mutation(["hello", "HeLlo"]) +console.log(checkWords) +const checkWordsFalse = mutation(["hello", "hey"]) +console.log(checkWordsFalse) +const checkWordsTrue = mutation(["Alien", "line"]) +console.log(checkWordsTrue) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/mutations-algorithm.md b/fullstack/javascript/6-loops/mutations-algorithm.md new file mode 100644 index 0000000..e0f534d --- /dev/null +++ b/fullstack/javascript/6-loops/mutations-algorithm.md @@ -0,0 +1,11 @@ +# Implement the Mutations Algorithm + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Create a function named mutation that takes an array as its argument. +2. [x] mutation should return true if the string in the first element of the array contains all of the letters of the string in the second element of the array, and false otherwise. For example: + - mutation(["hello", "Hello"]), should return true because all of the letters in the second string are present in the first, ignoring case. + - mutation(["hello", "hey"]) should return false because the string hello does not contain a y. + - mutation(["Alien", "line"]), should return true because all of the letters in line are present in Alien. diff --git a/fullstack/javascript/6-loops/profile-lookup.js b/fullstack/javascript/6-loops/profile-lookup.js new file mode 100644 index 0000000..285403d --- /dev/null +++ b/fullstack/javascript/6-loops/profile-lookup.js @@ -0,0 +1,41 @@ +const concactList = [ + { lastName: "Jensen", active: true }, + { name: "Akira", lastName: "Laine" }, + { name: "Akira", lastName: "Smith", age: 28 }, + { name: "Yoandy", lastName: "Doble", age: 37, city: "Cuba" }, + { name: "Kelly" }, + { lastName: "Blake" }, +] +/** + * Function that retrieve information property from contact list. + * @param {String} name Contact name to lookup. + * @param {String} property Property name to retrieve. + * @returns {String} If the function receives a contact name and the property exists on the related contact, then the property's value will be returned. If the name passed to the function does not match any contacts in the contacts array, then the function return "No such contact". If the property does not exist on a found contact, then the function return "No such property". + */ +const lookUpProfile = (name, property) => { + let people = concactList.filter((contact) => { + return contact["name"] === name + }) + if (people.length > 0) { + const peopleProperty = people + .reduce((acc, objContact) => { + if (property in objContact) acc.push(objContact[property]) + return acc + }, []) + .join(",") + if (peopleProperty.length > 0) { + return peopleProperty + } else { + return "No such property" + } + } else { + return "No such contact" + } +} + +// --- Test Cases --- +console.log("--- Profile Lookup ---") +console.log(">>>Contact property:\n", lookUpProfile("Akira", "lastName")) +console.log(">>>Missed property:\n", lookUpProfile("Yoandy", "active")) +console.log(">>>Missed contact:\n", lookUpProfile("Kevin", "lastName")) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/profile.lookup.md b/fullstack/javascript/6-loops/profile.lookup.md new file mode 100644 index 0000000..59035bc --- /dev/null +++ b/fullstack/javascript/6-loops/profile.lookup.md @@ -0,0 +1,14 @@ +# Build a Profile Lookup + +In this lab, you will build a profile lookup that looks up information about people in a contacts list. +For this example imagine there is a contact named Akira Laine, the lookUpProfile("Akira", "lastName") should return Laine. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named lookUpProfile that takes a name and a property as arguments. +2. [x] You should retrieve contact information from the provided contacts array. +3. [x] If the function receives a contact name and the property exists on the related contact, then the property's value should be returned. +4. [x] If the name passed to the function does not match any contacts in the contacts array, then the function should return "No such contact". +5. [x] If the property does not exist on a found contact, then the function should return "No such property". diff --git a/fullstack/javascript/6-loops/string-repeating.js b/fullstack/javascript/6-loops/string-repeating.js new file mode 100644 index 0000000..6daef5f --- /dev/null +++ b/fullstack/javascript/6-loops/string-repeating.js @@ -0,0 +1,20 @@ +/** + * Function that repeats a given string a specific number of times. + * @param {String} sentence any string. + * @param {Number} times specific number of times. + * @returns {String} repeated string. + */ +const repeatStringNumTimes = (sentence, times) => { + if (times <= 0) return "" + let result = [] + for (let index = 0; index < times; index++) { + result.push(sentence) + } + return result.join(" ") +} + +// --- Test Cases --- +console.log("--- String Repeating Function ---") +console.log(">>>Repeat 5 times:\n", repeatStringNumTimes("Akira", 5)) +console.log(">>>Empty string:\n", repeatStringNumTimes("Akira", 0)) +console.log("\n====================================") diff --git a/fullstack/javascript/6-loops/string-repeating.md b/fullstack/javascript/6-loops/string-repeating.md new file mode 100644 index 0000000..7592c9c --- /dev/null +++ b/fullstack/javascript/6-loops/string-repeating.md @@ -0,0 +1,11 @@ +# Build a String Repeating Function + +In this lab, you will create a function that repeats a given string a specific number of times. For the purpose of this lab, do not use the built-in .repeat() method. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named repeatStringNumTimes that takes two parameters: a string and a number. +2. [x] The function should return the string repeated the specified number of times. +3. [x] If the number is less than or equal to zero, the function should return an empty string. diff --git a/fullstack/javascript/README.md b/fullstack/javascript/README.md new file mode 100644 index 0000000..c48e0e3 --- /dev/null +++ b/fullstack/javascript/README.md @@ -0,0 +1,38 @@ +# Javascript Certification + +This course teaches you core JavaScript programming concepts such as working with variables, functions, objects, arrays, and control flow. You'll also learn how to manipulate the DOM, handle events, and apply techniques like asynchronous programming, functional programming, and accessibility best practices. + +To earn your **JavaScript Certification**: + +- Complete the five required projects to qualify for the certification exam. + +- Pass the JavaScript Certification exam. + +## Variables and Strings + +**Datatypes**: There are 7. + +- Number +- String +- Boolean +- Undefined +- Null +- Objects +- Symbol +- BigInt + +**Variables** +let and const + +let user = "codebydoble" + +const maxScore = 100; + +**Excersices** +Practice string section. + +- [x] Greeting Bot +- [x] Teacher Chatbot +- [x] String Inspector +- [x] String Formatter +- [x] String Transformer diff --git a/fullstack/javascript/js-fundamentals/dna-pair-generator.js b/fullstack/javascript/js-fundamentals/dna-pair-generator.js new file mode 100644 index 0000000..7ddfba9 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/dna-pair-generator.js @@ -0,0 +1,20 @@ +/** + * Function that converts a string DNA into a DNA pair array. + * @param {String} srt a string of any length + * @returns {Array} a new array, where each inner array has two strings inside, the first string is one base from the input, and the second string the paired base. + - When given A, the function should pair it with T. + - When given T, the function should pair it with A. + - When given C, the function should pair it with G. + - When given G, the function should pair it with C. + */ +const pairElement = (srt) => { + const pairs = { A: "T", T: "A", C: "G", G: "C" } + const srtToArr = srt.split("") + const srtMapped = srtToArr.map((letter) => { + return [letter, pairs[letter]] + }) + return srtMapped +} + +const dnaGenerated = pairElement("ATCG") // return [["A", "T"], ["T", "A"], ["C", "G"], ["G", "C"]] +console.log(dnaGenerated) diff --git a/fullstack/javascript/js-fundamentals/dna-pair-generator.md b/fullstack/javascript/js-fundamentals/dna-pair-generator.md new file mode 100644 index 0000000..c794466 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/dna-pair-generator.md @@ -0,0 +1,17 @@ +# Implement a DNA Pair Generator + +In the double helix of the DNA, the bases are always paired together: if on one strand there is an A base, on the other strand directly in front there is a T base, the other pair is C and G. +In this lab, you will write a function to match the missing base pairs for the provided DNA strand. For each character in the provided string, find the base pair character. +For example, for the input ATCG, return [["A", "T"], ["T", "A"], ["C", "G"], ["G", "C"]] +The A base gets paired with a T base, the T base is paired with a A base, the C is paired with the G base, and finally the G base is paired with a C base. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a pairElement function that takes a string of any length as an argument. +2. [x] The pairElement function should return a 2d array, where each inner array has two strings inside, the first string is one base from the input, and the second string the paired base. +3. [x] When given A, the function should pair it with T. +4. [x] When given T, the function should pair it with A. +5. [x] When given C, the function should pair it with G. +6. [x] When given G, the function should pair it with C. diff --git a/fullstack/javascript/js-fundamentals/element-skipper.js b/fullstack/javascript/js-fundamentals/element-skipper.js new file mode 100644 index 0000000..eaa33b6 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/element-skipper.js @@ -0,0 +1,20 @@ +/** + * Function that skips elements in an array until it finds an acceptable one based on a specific test function. + * @param {Array} arr any array. + * @param {Function} fn any function. + * @returns {Array} an array until it finds an acceptable one based on a specific test function. + */ +const dropElements = (arr, func) => { + for (const [key, value] of arr.entries()) { + if (func(value)) { + return arr.slice(key) + } + } + return [] +} + +const elements = [1, 1, 1, 2, 1, 1, 1] +const myFn = (n) => n === 2 + +const resultArr = dropElements(elements, myFn) +console.log(resultArr) diff --git a/fullstack/javascript/js-fundamentals/element-skipper.md b/fullstack/javascript/js-fundamentals/element-skipper.md new file mode 100644 index 0000000..6fd1a8d --- /dev/null +++ b/fullstack/javascript/js-fundamentals/element-skipper.md @@ -0,0 +1,13 @@ +# Implement an Element Skipper + +In this lab you will create a function that skips elements in an array until it finds an acceptable one based on a specific test function. +For example, for an array like [1, 1, 1, 2, 1, 1, 1] and a test function function(n) {return n === 2}, the first element that is acceptable for this is the one at index 3, so all the elements before that need to be discarded, and the output should be the remaining elements [2, 1, 1, 1]. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a dropElements function that accepts an array (arr) and a function (func) as arguments. +2. [x] The dropElements function should iterate through the array and remove elements starting from the first one until func returns true for an element. +3. [x] The dropElements function should return the remaining elements in the array if the condition is met. +4. [x] If the condition is never satisfied, it should return an empty array. diff --git a/fullstack/javascript/js-fundamentals/exam/js-fundamentals-exam.js b/fullstack/javascript/js-fundamentals/exam/js-fundamentals-exam.js new file mode 100644 index 0000000..16da6ef --- /dev/null +++ b/fullstack/javascript/js-fundamentals/exam/js-fundamentals-exam.js @@ -0,0 +1,193 @@ +/** + * ======================================== + * EXAM 3: JAVASCRIPT FUNDAMENTALS + * ======================================== + * Focus: freeCodeCamp Basic JavaScript (First ~70 challenges) + * Topics: Variables, Coercion, Strings, Basic Arrays, Control Flow + * Rules: Vanilla JS only. + * Run the file to see if your tests pass. + * ======================================== + */ +"use strict" +console.log("=== Part 1: Type Coercion & Equality (Predictions) ===") + +// ── Q1: Coercion & Operators (4 pts) ── +console.log("\n<<<===>>> Question 1 <<<===>>>\n") +// Predict the output of each line. Write your answer as a comment. +console.log("5" + 5) // Your prediction: 55 +console.log("5" - 2) // Your prediction: 3 +console.log(8 % 3) // Your prediction: 2 +console.log(typeof null) // Your prediction: object + +// ── Q2: Equality & Scope (4 pts) ── +console.log("\n<<<===>>> Question 2 <<<===>>>\n") +// Predict the output. +console.log(3 == "3") // Your prediction: true +console.log(3 === "3") // Your prediction: false + +let a = 10 +function changeValue() { + let a = 20 + return a +} +console.log(changeValue()) // Your prediction: 20 +console.log(a) // Your prediction: 10 + +// ── Q3: Const Mutation (2 pts) ── +console.log("\n<<<===>>> Question 3 <<<===>>>\n") +// Predict what happens when this code runs. +const myArr = [1, 2, 3] +myArr[0] = 99 +console.log(myArr) // Your prediction: [99, 2, 3] + +console.log("\n=== Part 2: Strings & Arrays ===") + +// ── Q4: String Manipulation ── +console.log("\n<<<===>>> Question 4 <<<===>>>\n") +/** + * Write a function that takes a string and a number 'n'. + * Return the 'n'th-to-last character of the string. + * Example: nthToLast("freeCodeCamp", 2) -> "m" + * @param {String} str any sentence. + * @param {Number} n + * @returns {String} Return the 'n'th-to-last character of the string. + */ +function nthToLast(str, n) { + try { + if (n > str.length || n <= 0) { + throw new RangeError("Range Error") + } + return str[str.length - n] + } catch (error) { + return `${error.message}` + } +} + +// Tests Q4: +console.log(nthToLast("JavaScript", 1)) // expected: "t" +console.log(nthToLast("Hello World", 3)) // expected: "r" +console.log(nthToLast("freeCodeCamp", 2)) // expected: "m" + +// ── Q5: Stand in Line (Queue) ── +console.log("\n<<<===>>> Question 5 <<<===>>>\n") +/** + * Write a function nextInLine which takes an array (arr) and a number (item). + * Add the number to the end of the array, then remove the first element of the array. The nextInLine function should then return the element that was removed. + * @param {Array} arr any array. + * @param {Number} item any item to add into array. + * @returns {Number|String|Boolean} return the element that was removed. + */ +function nextInLine(arr, item) { + if (arr.length === 0) { + arr.push(item) + return "" + } + const removed = arr.shift() + arr.push(item) + return removed +} + +// Tests Q5: +let testArr = [1, 2, 3, 4, 5] +let resultNextInLine = nextInLine(testArr, 6) +console.log(">>>NextInLine ", resultNextInLine) // expected: 1 +console.log(testArr) // expected: [2, 3, 4, 5, 6] + +console.log("\n=== Part 3: Control Flow ===") + +// ── Q6: Golf Code (Multiple Conditions) ── +console.log("\n<<<===>>> Question 6 <<<===>>>\n") +/** + * In the game of Golf, each hole has a par, meaning the average number of strokes a golfer is expected to make. Depending on how far above or below par your strokes are, there is a different nickname. + * Write a function that takes 'par' and 'strokes' arguments. Return the correct + * string according to this table: + * * Strokes | Return + * 1 | "Hole-in-one!" + * <= par - 2 | "Eagle" + * par - 1 | "Birdie" + * par | "Par" + * par + 1 | "Bogey" + * par + 2 | "Double Bogey" + * >= par + 3 | "Go Home!" + * @param {Array} par any array. + * @param {Number} strokes any item to add into array. + * @returns {String} return the element that was removed. + */ +function golfScore(par, strokes) { + let decision + if (strokes === 1) { + decision = "Hole-in-one!" + } else if (strokes <= par - 2) { + decision = "Eagle" + } else if (strokes === par - 1) { + decision = "Birdie" + } else if (strokes === par) { + decision = "Par" + } else if (strokes === par + 1) { + decision = "Bogey" + } else if (strokes === par + 2) { + decision = "Double Bogey" + } else if (strokes >= par + 3) { + decision = "Go Home!" + } + return decision +} + +// Tests Q6: +console.log(golfScore(4, 1)) // expected: "Hole-in-one!" +console.log(golfScore(4, 2)) // expected: "Eagle" +console.log(golfScore(5, 4)) // expected: "Birdie" +console.log(golfScore(4, 4)) // expected: "Par" +console.log(golfScore(4, 7)) // expected: "Go Home!" + +// ── Q7: Switch Statements ── +console.log("\n<<<===>>> Question 7 <<<===>>>\n") +/** + * Write a function that takes a lowercase string representing a day of the week. + * Use a switch statement to return the following: + * "monday" -> "Start of the week" + * "tuesday", "wednesday", "thursday" -> "Midweek" + * "friday" -> "Almost weekend" + * "saturday", "sunday" -> "Weekend" + * Anything else -> "Invalid day" + */ + +/** + * Write a function that takes a lowercase string representing a day of the week. Use a switch statement to return the following: + * "monday" -> "Start of the week" + * "tuesday", "wednesday", "thursday" -> "Midweek" + * "friday" -> "Almost weekend" + * "saturday", "sunday" -> "Weekend" + * Anything else -> "Invalid day" + * @param {String} day + */ +function dayClassifier(day) { + let decision + switch (day.toLowerCase()) { + case "monday": + decision = "Start of the week" + break + case "tuesday": + case "wednesday": + case "thursday": + decision = "Midweek" + break + case "friday": + decision = "Almost weekend" + break + case "saturday": + case "sunday": + decision = "Weekend" + break + default: + decision = "Invalid day" + break + } + return decision +} + +// Tests Q7: +console.log(dayClassifier("wednesday")) // expected: "Midweek" +console.log(dayClassifier("thursday")) // expected: "Midweek" +console.log(dayClassifier("saturday")) // expected: "Weekend" +console.log(dayClassifier("funday")) // expected: "Invalid day" diff --git a/fullstack/javascript/js-fundamentals/exam/oddFibonacci.js b/fullstack/javascript/js-fundamentals/exam/oddFibonacci.js new file mode 100644 index 0000000..16a691b --- /dev/null +++ b/fullstack/javascript/js-fundamentals/exam/oddFibonacci.js @@ -0,0 +1,26 @@ +function sumFibs(num) { + // 1. Set up our starting variables + let previous = 0; + let current = 1; + let sum = 0; + + // 2. Keep looping as long as the current number is less than or equal to our limit + while (current <= num) { + + // 3. Check if the current number is odd + if (current % 2 !== 0) { + sum += current; // Add it to our running total + } + + // 4. Calculate the next numbers in the Fibonacci sequence + // We store the 'current' in a temporary variable so we don't lose it + let nextNumber = current + previous; + previous = current; + current = nextNumber; + } + + // 5. Return the final total + return sum; +} + +console.log(sumFibs(10)); // Output: 10 \ No newline at end of file diff --git a/fullstack/javascript/js-fundamentals/falsy-remover.js b/fullstack/javascript/js-fundamentals/falsy-remover.js new file mode 100644 index 0000000..adf20ee --- /dev/null +++ b/fullstack/javascript/js-fundamentals/falsy-remover.js @@ -0,0 +1,14 @@ +/** + * Function that removes all falsy values from an array. +Falsy values in JavaScript are: false, null, 0, "", undefined, and NaN. + * @param {Array} arr any Array. + * @returns {Array} new array without falsy values. + */ +const bouncer = (arr) => { + const falsy = new Set([false, null, 0, "", undefined, NaN]) + return arr.filter((element) => { + if (!falsy.has(element)) return element + }) +} + +console.log(bouncer(["codebydoble", false, 2.45, true, [45, 68], undefined, { name: "Yoandy" }, null])) diff --git a/fullstack/javascript/js-fundamentals/falsy-remover.md b/fullstack/javascript/js-fundamentals/falsy-remover.md new file mode 100644 index 0000000..09a6d8a --- /dev/null +++ b/fullstack/javascript/js-fundamentals/falsy-remover.md @@ -0,0 +1,14 @@ +# Implement a Falsy Remover + +In this lab you will create a function that removes all falsy values from an array. +Falsy values in JavaScript are **false, null, 0, "", undefined, and NaN**. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a bouncer function that takes an array as argument. +2. [x] The bouncer function should return a new array that contains the same elements as the array passed in as argument with the falsy elements removed. +3. [x] The bouncer function should not change the array passed in as an argument. + +**Hint**: Try converting each value to a Boolean. diff --git a/fullstack/javascript/js-fundamentals/first-element-finder.js b/fullstack/javascript/js-fundamentals/first-element-finder.js new file mode 100644 index 0000000..a84a52f --- /dev/null +++ b/fullstack/javascript/js-fundamentals/first-element-finder.js @@ -0,0 +1,18 @@ +/** + * Function that looks through an array and returns the first element that passes a test function + * @param {Array} arr any array. + * @param {Function} fn function test. + * @returns {Number|String|undefined} the first element where the test function returns true. + */ +const findElement = (arr, fn) => { + const passed = arr.filter((element) => { + return fn(element) + }) + return passed.length > 0 ? passed[0] : undefined +} + +/* --- Test Cases --- */ +console.log("--- String Inverter ---") +console.log(findElement([1, 3, 5, 8], (num) => num % 2 === 0)) // returns 8 +console.log(findElement([1, 3, 5], (num) => num % 2 === 0)) // returns undefined +console.log("\n====================================") diff --git a/fullstack/javascript/js-fundamentals/first-element-finder.md b/fullstack/javascript/js-fundamentals/first-element-finder.md new file mode 100644 index 0000000..59d9c4f --- /dev/null +++ b/fullstack/javascript/js-fundamentals/first-element-finder.md @@ -0,0 +1,15 @@ +# Build a First Element Finder + +In this lab, you will create a function that looks through an array and returns the first element that passes a test function (also known as a "truth test"). +The function would iterate through the array and test each element using the provided test function. At the end, it would return the first element where the test function returns true. +Example: +findElement([1, 3, 5, 8], num => num % 2 === 0) // returns 8 +findElement([1, 3, 5], num => num % 2 === 0) // returns undefined + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named findElement that accepts an array and a function as arguments. +2. [x] The function should return the first item in the array that passes a truth test. This means that, calling the passed in function func, given an element x, the truth test is passed if func(x) is true. +3. [x] If no element passes the test, the function should return undefined. diff --git a/fullstack/javascript/js-fundamentals/gradebook-app.js b/fullstack/javascript/js-fundamentals/gradebook-app.js new file mode 100644 index 0000000..3cd834d --- /dev/null +++ b/fullstack/javascript/js-fundamentals/gradebook-app.js @@ -0,0 +1,81 @@ +/** + * Function that calculates average notes. + * @param {Array} arr any notes array. + * @returns {Number} average notes. + */ +const getAverage = (arr) => { + if (arr.length === 0) return 0 + return arr.reduce((avg, note, index, entireArr) => { + avg += note + if (index === entireArr.length - 1) { + avg /= entireArr.length + } + return avg + }, 0) +} + +/** + * Function that takes a student score as a parameter and returns a string representing a letter grade based on the score. Here are the scores and their corresponding letter grades: + Score Range + Grade + 100 "A+" + 90 - 99 "A" + 80 - 89 "B" + 70 - 79 "C" + 60 - 69 "D" + 0 - 59 "F" + * @param {Number} score a student score . + * @returns {String} a string representing a letter grade based on the score. + */ +const getGrade = (score) => { + if (score === 100) { + return "A+" + } else if (score <= 99 && score >= 90) { + return "A" + } else if (score <= 89 && score >= 80) { + return "B" + } else if (score <= 79 && score >= 70) { + return "C" + } else if (score <= 69 && score >= 60) { + return "D" + } else { + return "F" + } +} + +/** + * Function that takes a score as a parameter and returns either true or false depending on if the score corresponds to a passing grade. + * @param {Number} score average student notes. + * @returns {Boolean} true or false depending on if score corresponds to a passing grade. + */ +const hasPassingGrade = (score) => { + const grade = getGrade(score) + return grade !== "F" ? true : false +} + +/** + * + * @param {Array} scores + * @param {Number} studentScore + * @returns {String} + */ +const studentMsg = (scores, studentScore) => { + const average = getAverage(scores) + const grade = getGrade(studentScore) + let result = `Class average: ${average}. Your grade: ${grade}.` + console.log(">>>msg passing", hasPassingGrade(average)) + hasPassingGrade(average) ? (result += " You passed the course.") : (result += " You failed the course.") + return result +} + +/* --- Test Cases --- */ +console.log("--- Gradebook App ---") +const studentScore = getAverage([98, 63, 95, 94, 87, 88]) +console.log(">>>Student Score", studentScore) // returns 87.5 +const studentGrade = getGrade(studentScore) +console.log(">>>Grade", studentGrade) // returns "B" +const studentPassing = hasPassingGrade(studentScore) +console.log(">>>is Passing Grade", studentPassing) // returns true +const msg = studentMsg([98, 63, 95, 94, 87, 88], 87.5) +console.log(msg) +console.log("\n====================================") diff --git a/fullstack/javascript/js-fundamentals/gradebook.md b/fullstack/javascript/js-fundamentals/gradebook.md new file mode 100644 index 0000000..3fd353e --- /dev/null +++ b/fullstack/javascript/js-fundamentals/gradebook.md @@ -0,0 +1,29 @@ +# Build a Gradebook App + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named getAverage that takes an array of test scores as a parameter and returns the average score. +2. [x] You should have a function named getGrade that takes a student score as a parameter and returns a string representing a letter grade based on the score. Here are the scores and their corresponding letter grades: + Score Range + Grade + 100 + "A+" + 90 - 99 + "A" + 80 - 89 + "B" + 70 - 79 + "C" + 60 - 69 + "D" + 0 - 59 + "F" +3. [x] You should have a function named hasPassingGrade that takes a score as a parameter and returns either true or false depending on if the score corresponds to a passing grade. +4. [x] The hasPassingGrade function should use the getGrade function to get the letter grade, and use it to determine if the grade is passing. A passing grade is anything different from "F". +5. [x] You should have a function named studentMsg that takes an array of scores and a student score as the parameters. The function should return a string with the format: + - "Class average: average-goes-here. Your grade: grade-goes-here. You passed the course.", if the student passed the course. + - "Class average: average-goes-here. Your grade: grade-goes-here. You failed the course.", if the student failed the course. + +Replace **average-goes-here** with the **average of total scores** and **grade-goes-here** with the **student's grade**. Use **getAverage** to get the average score and **getGrade** to get the student's grade. diff --git a/fullstack/javascript/js-fundamentals/html-entity-converter.js b/fullstack/javascript/js-fundamentals/html-entity-converter.js new file mode 100644 index 0000000..af7d648 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/html-entity-converter.js @@ -0,0 +1,26 @@ +/** + * Function that converts special characters in a string with their corresponding HTML entities. + * @param {String} sentence any string. + * @returns {String} a new string with their corresponding HTML entities. + */ +const convertHTML = (sentence) => { + const entity = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" } + return sentence + .trim() + .split("") + .map((token) => { + if (token in entity) return entity[token] + return token + }) + .join("") +} + +// Test cases +console.log(convertHTML("alert('XSS') Bold text \"test\" ")) +// Expected: "alert('XSS') <b>Bold text "test" </b>" + +console.log(convertHTML(" Normal comment ")) +// Expected: "Normal comment" + +console.log(convertHTML("Quote: 'test' and \"test\"")) +// Expected: "Quote: 'test' and "test"" diff --git a/fullstack/javascript/js-fundamentals/html-entity-converter.md b/fullstack/javascript/js-fundamentals/html-entity-converter.md new file mode 100644 index 0000000..a43621a --- /dev/null +++ b/fullstack/javascript/js-fundamentals/html-entity-converter.md @@ -0,0 +1,16 @@ +# Implement an HTML Entity Converter + +This lab is about converting special characters in a string with their corresponding HTML entities. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a convertHTML function that accepts a string as an argument. +2. [x] The convertHTML function should return a new string by converting special characters in the argument string to their corresponding HTML entities. + + - & should be converted to &. + - < should be converted to <. + - >  should be converted to >. + - " should be converted to ". + - ' should be converted to '. diff --git a/fullstack/javascript/js-fundamentals/inventory-managment-program.js b/fullstack/javascript/js-fundamentals/inventory-managment-program.js new file mode 100644 index 0000000..6b2cba5 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/inventory-managment-program.js @@ -0,0 +1,80 @@ +const inventory = [ + { name: "mouse", quantity: 20 }, + { name: "speaker", quantity: 50 }, + { name: "keyboard", quantity: 20 }, + { name: "notebook", quantity: 13 }, + { name: "laptop", quantity: 12 }, + { name: "smartphone", quantity: 100 }, + { name: "pen", quantity: 50 }, +] + +/** + * Function that takes the product name as its argument and returns the index of the corresponding product object inside the inventory array. The function should always use the lowercase form of the product name to perform the search. If the product is not found, the function should return -1. + * @param {String} productName any product name to perform the search. + * @returns {Number} the index of the corresponding product object inside the inventory array. If the product is not found, the function should return -1. + */ +const findProductIndex = (productName) => { + if (inventory.length === 0) return -1 + return inventory.findIndex((product, index) => { + if (product.name === productName.toLowerCase()) return index + }) +} + +/** + * Function that insert or update a product at inventory array. + * @param {Object} productObj any product. + * @returns {String} If the product is already present in the inventory, the addProduct function should update its quantity value by adding the quantity passed in to the function to the current quantity and log to the console the product name followed by a space and quantity updated. If the product is not present in the inventory, the addProduct function should push the product to the inventory array and log the product name to the console, followed by a space and added to inventory. + */ +const addProduct = (productObj) => { + const indexProduct = findProductIndex(productObj.name) + if (indexProduct !== -1) { + // 1. update product + inventory[indexProduct].quantity = productObj.quantity + console.log(`${inventory[indexProduct].name} ${inventory[indexProduct].quantity}`) + } else { + // 2. Destructuring obj + const { name, quantity } = productObj + + // 3. add product + inventory.push({ name: name.toLowerCase(), quantity: quantity }) + console.log(`${inventory[inventory.length - 1].name} added to inventory.`) + } +} + +/** + * + * @param {String} productName any product name. + * @param {Number} quantity amount of product name to subtract. + * @return The removeProduct function should subtract the passed quantity from the corresponding product object inside the inventory and log the string Remaining pieces:  to the console, where  should be replaced by the product name, and  should be replaced by the product quantity. If the quantity after the subtraction is zero, removeProduct should remove the product object from the inventory. If the quantity in the inventory is not enough to perform the subtraction, the removeProduct function should log the string Not enough available, remaining pieces:  to the console. + */ +const removeProduct = (productName, quantity) => { + const indexProduct = findProductIndex(productName.toLowerCase()) + if (indexProduct !== -1) { + // appears + if (inventory[indexProduct].quantity === quantity) { + // 1. remove obj - subtract equal to zero + inventory.splice(indexProduct, 1) + console.log("Object removed subtract equal to zero") + } else if (inventory[indexProduct].quantity < quantity) { + // 2. the quantity in the inventory is not enough to perform the subtraction + console.log(`Not enough ${inventory[indexProduct].name} available, remaining pieces: ${inventory[indexProduct].quantity}`) + } else { + // 3. Subtract quantity + inventory[indexProduct].quantity -= quantity + console.log(`Remaining ${inventory[indexProduct].name} pieces: ${inventory[indexProduct].quantity}`) + } + } else { + // product not found + console.log(`${productName} not found`) + } +} + +console.log(">>>Find Product Index:", findProductIndex("LAPtop")) +addProduct({ name: "mousepad", quantity: 15 }) +addProduct({ name: "PEN", quantity: 43 }) +addProduct({ name: "ChIp", quantity: 28 }) +addProduct({ name: "laptop", quantity: 56 }) +removeProduct("notebOOK", 13) +removeProduct("laptop", 68) +removeProduct("PEN", 3) +console.log(">>>Inventory:", inventory) diff --git a/fullstack/javascript/js-fundamentals/inventory-managment-program.md b/fullstack/javascript/js-fundamentals/inventory-managment-program.md new file mode 100644 index 0000000..0ec1e17 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/inventory-managment-program.md @@ -0,0 +1,19 @@ +# Build an Inventory Management Program + +In this lab, you will build an inventory management program that will allow you to add, update, find and remove products from the inventory. You will use an array of objects to represent your inventory, where each object will have name and quantity as the keys. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should declare an empty array named inventory that will store product objects having a key name with the value of a lowercase string, and a key quantity with the value of an integer. +2. [x] You should create a function named findProductIndex that takes the product name as its argument and returns the index of the corresponding product object inside the inventory array. The function should always use the lowercase form of the product name to perform the search. If the product is not found, the function should return -1. +3. [x] You should create a function named addProduct that takes a product object as its argument. +4. [x] If the product is already present in the inventory, the addProduct function should update its quantity value by adding the quantity passed in to the function to the current quantity and log to the console the product name followed by a space and quantity updated. +5. [x] If the product is not present in the inventory, the addProduct function should push the product to the inventory array and log the product name to the console, followed by a space and added to inventory. +6. [x] You should create a function named removeProduct that takes the product name and quantity as its arguments. +7. [x] The removeProduct function should subtract the passed quantity from the corresponding product object inside the inventory and log the string Remaining pieces:  to the console, where  should be replaced by the product name, and  should be replaced by the product quantity. +8. [x] If the quantity after the subtraction is zero, removeProduct should remove the product object from the inventory. If the quantity in the inventory is not enough to perform the subtraction, the removeProduct function should log the string Not enough available, remaining pieces:  to the console. +9. [x] If the product to remove is not present in the inventory, the removeProduct function should log  not found to the console. + +**Note**: To prevent conflicts, keep only the logging mentioned in the user stories when running tests. diff --git a/fullstack/javascript/js-fundamentals/largest-number-finder.js b/fullstack/javascript/js-fundamentals/largest-number-finder.js new file mode 100644 index 0000000..988a86d --- /dev/null +++ b/fullstack/javascript/js-fundamentals/largest-number-finder.js @@ -0,0 +1,19 @@ +/** + * Function that returns an array consisting of the largest number from each provided sub-array. + * @param {Array} arr any array of arrays. + * @returns {Array} an array consisting of the largest number from each provided sub-array. + */ +const largestOfAll = (arr) => { + const largestNumbers = [] + arr.forEach((item) => { + if (Array.isArray(item)) { + largestNumbers.push(Math.max(...item)) + } + }) + return largestNumbers +} + +// --- Test Cases --- +console.log("--- Largest Number Finder ---") +console.log(largestOfAll([[3, 1, 9, 4], [5], [93, 66, 5, 8], [3, 69, 741, 2577, 4, 1, 8]])) +console.log("\n====================================") diff --git a/fullstack/javascript/js-fundamentals/largest-number-finder.md b/fullstack/javascript/js-fundamentals/largest-number-finder.md new file mode 100644 index 0000000..b30dce4 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/largest-number-finder.md @@ -0,0 +1,11 @@ +# Build the Largest Number Finder + +In this lab, you will build a function that returns an array consisting of the largest number from each provided sub-array. +Remember, you can iterate through an array with a simple for loop, and access each member with array syntax arr[i]. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function largestOfAll that takes an array of arrays as an argument. +2. [x] The function should return an array containing the largest number from each sub-array. diff --git a/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.js b/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.js new file mode 100644 index 0000000..cab83c4 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.js @@ -0,0 +1 @@ +const sumFibs = (num) => {} diff --git a/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.md b/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.md new file mode 100644 index 0000000..b8389b5 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/odd-fibonacci-sum-calculator.md @@ -0,0 +1,12 @@ +# Build an Odd Fibonacci Sum Calculator + +In this lab you will build an odd Fibonacci sum calculator that computes the sum of all odd Fibonacci numbers that are less than or equal to a given positive integer. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a sumFibs function that accepts a number as an argument. +2. [] The sumFibs function should return the sum of all odd Fibonacci numbers that are less than or equal to the given number. +3. [] The Fibonacci sequence starts with 0 and 1, and each subsequent number is the sum of the two previous ones. +4. [] Only the odd Fibonacci numbers should be added to the sum. diff --git a/fullstack/javascript/js-fundamentals/password-generator-app.js b/fullstack/javascript/js-fundamentals/password-generator-app.js new file mode 100644 index 0000000..df88199 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/password-generator-app.js @@ -0,0 +1,16 @@ +/** + * Function that randomly generate a password. + * @param {Number} longitud password length. + * @returns {String} password generated. + */ +const generatePassword = (longitud) => { + const pass = [] + const base = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&\*()` + for (let index = 0; index < longitud; index++) { + pass.push(base[Math.floor(Math.random() * base.length)]) + } + return pass.join("") +} + +const password = generatePassword(10) +console.log(`Generated password: ${password}`) diff --git a/fullstack/javascript/js-fundamentals/password-generator-app.md b/fullstack/javascript/js-fundamentals/password-generator-app.md new file mode 100644 index 0000000..034f742 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/password-generator-app.md @@ -0,0 +1,12 @@ +# Build a Password Generator App + +In this lab, you'll practice using functions by building a random password generator. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function called generatePassword that takes a parameter, indicating the length of generated password. You can name the parameter whatever you like. +2. [x] Your function should return a string which represents a randomly generated password. You should use the following string and different Math methods to help you return a new string with random characters in it: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&\*(). +3. [x] You should define a variable called password and assign it the result of calling the generatePassword function with a numeric argument that represents the desired password length. +4. [x] You should have a console.log that logs a single string made by concatenating the message Generated password: and the password variable separated by a space. diff --git a/fullstack/javascript/js-fundamentals/pyramide-generator.js b/fullstack/javascript/js-fundamentals/pyramide-generator.js new file mode 100644 index 0000000..2b7f82c --- /dev/null +++ b/fullstack/javascript/js-fundamentals/pyramide-generator.js @@ -0,0 +1,28 @@ +/** + * + * @param {String} pattern + * @param {Number} rows + * @param {Boolean} invert + */ +const pyramid = (pattern, rows, invert) => { + const result = [] + let spaceCountUp = rows + let spaceCountDown = rows - 1 + if (invert) { + // 1. vertex facing downwards. + for (let index = rows - 1; index >= 0; index--) { + result.push( + " ".repeat(spaceCountDown - index) + pattern + pattern.repeat(index * 2) + " ".repeat(spaceCountDown - index) + "\n", + ) + } + } else { + // 2. vertex facing upwards. + for (let index = 0; index < rows; index++) { + result.push(" ".repeat(spaceCountUp - 1) + pattern + pattern.repeat(index * 2) + " ".repeat(spaceCountUp - 1) + "\n") + spaceCountUp-- + } + } + return result.join("") +} + +console.log(pyramid("o", 4, false)) diff --git a/fullstack/javascript/js-fundamentals/pyramide-generator.md b/fullstack/javascript/js-fundamentals/pyramide-generator.md new file mode 100644 index 0000000..8497ab7 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/pyramide-generator.md @@ -0,0 +1,22 @@ +# Build a Pyramid Generator + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named pyramid that takes three arguments. +2. [x] The first argument should be a string representing the pattern character to repeat in your pyramid. +3. [x] The second argument should be an integer representing the number of rows in the pyramid. +4. [x] The third argument should be a Boolean value. +5. [x] The pyramid function should return a string in which the pattern character is repeated and arranged to form a pyramid having the vertex facing upwards when the third argument is false. +6. [x] When the third argument is true the pyramid should have the vertex facing downwards. +7. [x] The vertex row should have a single pattern character, and each other row should have two pattern characters more than the previous one. +8. [x] Each row should start with a number of spaces sufficient to put the center character of each row in the same column and there should not be any spaces at the end of each row. +9. [x] The pyramid should start and end with a newline character. + +For example, calling pyramid("o", 4, false) should give this output: + +o +ooo +ooooo +ooooooo diff --git a/fullstack/javascript/js-fundamentals/slice-splice-algorith.js b/fullstack/javascript/js-fundamentals/slice-splice-algorith.js new file mode 100644 index 0000000..feaf598 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/slice-splice-algorith.js @@ -0,0 +1,18 @@ +/** + * Function + * @param {Array} firstArr any array. + * @param {Array} secondArr any array. + * @param {Number} index number . + * @returs + */ +const frankenSplice = (firstArr, secondArr, index) => { + if (index > secondArr.length - 1) return "Invalid position to insert array" + const secondArrCopy = [...secondArr] + secondArrCopy.splice(1, 0, ...firstArr) + return secondArrCopy +} + +// --- Test Cases --- +console.log("--- Slice Splice Algorithm ---") +console.log(frankenSplice([2, 5, -7, "@codebydoble", true, false], [3, "Yoandy", 4], 1)) +console.log("\n====================================") diff --git a/fullstack/javascript/js-fundamentals/slice-splice-algorith.md b/fullstack/javascript/js-fundamentals/slice-splice-algorith.md new file mode 100644 index 0000000..0a22803 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/slice-splice-algorith.md @@ -0,0 +1,11 @@ +# Implement the Slice and Splice Algorithm + +In this lab, you will need to create an algorithm to merge two arrays. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] Create a frankenSplice function that accepts two arrays and an index. +2. [x] Copy each element of the first array into the second array, in order, beginning at the given index, and return the resulting array. +3. [x] The input arrays should remain the same after the function runs. diff --git a/fullstack/javascript/js-fundamentals/string-inverter.js b/fullstack/javascript/js-fundamentals/string-inverter.js new file mode 100644 index 0000000..5db1fff --- /dev/null +++ b/fullstack/javascript/js-fundamentals/string-inverter.js @@ -0,0 +1,18 @@ +/** + * Simple string inverter manual no reverse o toreverse method. + * @param {String} str any sentence. + * @returns {String} reversed string. + */ +const reverseString = (str) => { + const result = [] + for (let index = str.length; index >= 0; index--) { + result.push(str[index]) + } + return result.join("") +} + +// --- Test Cases --- +console.log("--- String Inverter ---") +console.log(reverseString("--- String Inverter ---")) +console.log(reverseString("hello")) +console.log("\n====================================") diff --git a/fullstack/javascript/js-fundamentals/string-inverter.md b/fullstack/javascript/js-fundamentals/string-inverter.md new file mode 100644 index 0000000..b0085d9 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/string-inverter.md @@ -0,0 +1,11 @@ +# Build a String Inverter + +In this lab, you will build a simple string inverter that reverses the characters of a given string. +For example, "hello" should become "olleh". + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should create a function named reverseString that takes a string as an argument. +2. [x] The function should return the reversed string. diff --git a/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.js b/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.js new file mode 100644 index 0000000..0d9dd98 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.js @@ -0,0 +1,23 @@ +/** + * Function that accepts an array of two numbers. sumAll([n, m]) should return the sum of n and m plus the sum of all the numbers between them. The lowest number will not always come first. For example, sumAll([4,1]) should return 10 because sum of all the numbers between 1 and 4 (both inclusive) is 10. + * @param {Array} arr any array of two numbers. + * @returns {Number} return the sum of first and second number in array plus the sum of all the numbers between them. + */ +const sumAll = (arr) => { + const [numOne, numTwo] = arr + let result = 0 + if (numOne === numTwo) return numOne + numTwo + if (numOne < numTwo) { + for (let index = numOne; index <= numTwo; index++) { + result += index + } + } else { + for (let index = numTwo; index <= numOne; index++) { + result += index + } + } + return result +} + +console.log(sumAll([1, 4])) +console.log(sumAll([4, 1])) diff --git a/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.md b/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.md new file mode 100644 index 0000000..a4418fb --- /dev/null +++ b/fullstack/javascript/js-fundamentals/sum-all-numbers-algorithm.md @@ -0,0 +1,8 @@ +# Design a Sum All Numbers Algorithm + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named sumAll that accepts an array of two numbers. +2. [x] sumAll([n, m]) should return the sum of n and m plus the sum of all the numbers between them. The lowest number will not always come first. For example, sumAll([4,1]) should return 10 because sum of all the numbers between 1 and 4 (both inclusive) is 10. diff --git a/fullstack/javascript/js-fundamentals/title-case-converter.js b/fullstack/javascript/js-fundamentals/title-case-converter.js new file mode 100644 index 0000000..41964df --- /dev/null +++ b/fullstack/javascript/js-fundamentals/title-case-converter.js @@ -0,0 +1,25 @@ +/** + * Function that converts a string to title case. Title case means that the first letter of each word is capitalized and the rest of the word is in lower case. + * @param {String} sentence any string. + * @returns {String} string title case. + */ +const titleCase = (sentence) => { + return sentence + .split(/\s+/g) + .map((word) => { + if (word.length === 1) return word.toUpperCase() + const [element, toCase, ...rest] = word.split(/(\w+)/g) + if (toCase.length === 1) { + return element + toCase.toUpperCase() + rest + } else { + return element + toCase[0].toUpperCase() + toCase.slice(1) + rest + } + }) + .join(" ") +} + +console.log(titleCase("I like to code")) // return "I Like To Code". +console.log(titleCase("javaScript is fun")) // return "Javascript Is Fun" + +const srtCase = titleCase("I & like to code. Congrats! *x*yoandy**") +console.log(srtCase) diff --git a/fullstack/javascript/js-fundamentals/title-case-converter.md b/fullstack/javascript/js-fundamentals/title-case-converter.md new file mode 100644 index 0000000..45b7c1a --- /dev/null +++ b/fullstack/javascript/js-fundamentals/title-case-converter.md @@ -0,0 +1,13 @@ +# Build a Title Case Converter + +In this lab you will create a function that converts a string to title case. Title case means that the first letter of each word is capitalized and the rest of the word is in lower case. +**"Web Development Is Awesome"** is an example of a title cased string. + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a titleCase function that takes a string as an argument. +2. [x] The titleCase function should return a string with the first letter of each word capitalized and the rest of the word in lower case. +3. [x] titleCase("I like to code") should return "I Like To Code". +4. [x] titleCase("javaScript is fun") should return "Javascript Is Fun" diff --git a/fullstack/javascript/js-fundamentals/unique-sorted-union.js b/fullstack/javascript/js-fundamentals/unique-sorted-union.js new file mode 100644 index 0000000..0a534e2 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/unique-sorted-union.js @@ -0,0 +1,14 @@ +/** + * Function that accept two or more arrays as arguments and return a new array that contains unique values from the argument arrays, in the order they are first found in the arguments. + * @param {...Array} arrs two or more arrays. + * @returns {Array} a new array that contains unique values from the argument arrays, in the order they are first found in the arguments. + */ +const uniteUnique = (...arrs) => { + const unite = arrs.reduce((acc, arr) => { + return acc.concat(arr) + }, []) + const unique = new Set(unite) + return Array.from(unique) +} +console.log(uniteUnique([1, 2, 4], [2, 3, 5])) +console.log(uniteUnique([1, 2, 4], [2, 3, 5], [6], [7, 8, 9], [0, 1, 2, 3])) diff --git a/fullstack/javascript/js-fundamentals/unique-sorted-union.md b/fullstack/javascript/js-fundamentals/unique-sorted-union.md new file mode 100644 index 0000000..6d0e797 --- /dev/null +++ b/fullstack/javascript/js-fundamentals/unique-sorted-union.md @@ -0,0 +1,9 @@ +# Implement a Unique Sorted Union + +**Objective**: Fulfill the user stories below and get all the tests to pass to complete the lab. + +## User Stories + +1. [x] You should have a function named uniteUnique. +2. [x] The uniteUnique function should accept two or more arrays as arguments. +3. [x] The function should return a new array that contains unique values from the argument arrays, in the order they are first found in the arguments. For example, an input like [1, 2, 4], [2, 3, 5] would have an output of [1, 2, 4, 3, 5].