Wednesday, 16 December 2020

2020 24 days of #JavaScriptmas

Xmas can be a somewhat exciting time, but the weather is frightful. We've had snow already this year (IKR! We never get snow before January usually) and the heating has even come on a couple of times during the day! My gaffer at work introduced us to Advent of Code, which has been excellent, but my main go-to has been Scrimba's #JavaScriptmas.

I've been reading about Digital Inclusion in rural communities in my role as a Parish Councillor and so I am as keen as Scrimba to get the word out there about how much fun coding can be.

Now I've been doing this long enough to have picked up all sorts of bad habits and, at work, I have to support IE11, so I rarely get to play with arrow functions or template literals. Except when doing challenges like this, that is: so please do forgive the almost deliberate lack of clarity. I'll document my solutions below, and will continue to add to them as I go along:

Day 1

const candies = (children, candy) => Math.floor(candy / children) * children

Day 2

const depositProfit = (d, r, t) => {
    let y = 0
    while (d <= t) {
        y++
        d += (r / 100) * d
    }
    return y
}

Day 3

const chunkyMonkey = (values, size) => {
  var arr = []
  for (let i = 0, len = values.length; i < len; i += size){
    arr.push(values.slice(i, i + size))
  }
  return arr
}

Day 4

const centuryFromYear = num => Math.floor((!(num % 100) ? num - 1 : num) / 100) + 1

Day 5

//const reverseAString = str => str.split("").reverse().join("")
const reverseAString = (str) => str === "" ? "" : reverseAString(str.substr(1))  + str.charAt(0)

Day 6

const sortByLength = strs => strs.sort((a, b) => a.length - b.length)

Day 7

const countVowelConsonant = str => str.split("").reduce((a, c) => a + (!!~['a','e','i','o','u'].indexOf(c) ? 1 : 2), 0)

Day 8

I'm not going to put the code here, as it's enormous, but most of the heavy lifting is done via CSS with only a tiny bit of JS. I've used this before, and I can't remember where I got the original idea from, but thank you, whoever you are! I've made the odd change to the markup, but not much TBH , it was mainly converting it to SCSS.

Actually, I've changed my mind, here's the CSS:

body {
  background-color: #eee; }

#view[data-value="1"] > .dice {
  transform: rotateY(360deg) !important; }

#view[data-value="2"] > .dice {
  transform: rotateY(-90deg) !important; }

#view[data-value="6"] > .dice {
  transform: rotateY(180deg) !important; }

#view[data-value="4"] > .dice {
  transform: rotateY(90deg) !important; }

#view[data-value="3"] > .dice {
  transform: rotateX(-90deg) !important; }

#view[data-value="5"] > .dice {
  transform: rotateX(90deg) !important; }

#view {
  cursor: pointer;  
  width: 200px;
  height: 200px;
  margin: 100px;
  perspective: 600px; }
  #view .dice {
    width: 200px;
    height: 200px;
    position: absolute;
    transform-style: preserve-3d;
    transition: transform 1s; }
    #view .dice .side {
      position: absolute;
      width: 200px;
      height: 200px;
      background: #fff;
      box-shadow: inset 0 0 40px #ccc;
      border-radius: 40px; }
      #view .dice .side.inner {
        background: #e0e0e0;
        box-shadow: none; }
      #view .dice .side.front {
        transform: translateZ(100px); }
        #view .dice .side.front.inner {
          transform: translateZ(98px); }
      #view .dice .side.right {
        transform: rotateY(90deg) translateZ(100px); }
        #view .dice .side.right.inner {
          transform: rotateY(90deg) translateZ(98px); }
      #view .dice .side.back {
        transform: rotateY(180deg) translateZ(100px); }
        #view .dice .side.back.inner {
          transform: rotateX(-180deg) translateZ(98px); }
      #view .dice .side.left {
        transform: rotateY(-90deg) translateZ(100px); }
        #view .dice .side.left.inner {
          transform: rotateY(-90deg) translateZ(98px); }
      #view .dice .side.top {
        transform: rotateX(90deg) translateZ(100px); }
        #view .dice .side.top.inner {
          transform: rotateX(90deg) translateZ(98px); }
      #view .dice .side.bottom {
        transform: rotateX(-90deg) translateZ(100px); }
        #view .dice .side.bottom.inner {
          transform: rotateX(-90deg) translateZ(98px); }
      #view .dice .side .dot {
        position: absolute;
        width: 46px;
        height: 46px;
        border-radius: 23px;
        background: #444;
        box-shadow: inset 5px 0 10px #000; }
        #view .dice .side .dot.center {
          margin: 77px 0 0 77px; }
        #view .dice .side .dot.dtop {
          margin-top: 20px; }
        #view .dice .side .dot.dleft {
          margin-left: 134px; }
        #view .dice .side .dot.dright {
          margin-left: 20px; }
        #view .dice .side .dot.dbottom {
          margin-top: 134px; }
        #view .dice .side .dot.center.dleft {
          margin: 77px 0 0 20px; }
        #view .dice .side .dot.center.dright {
          margin: 77px 0 0 134px; }

Day 9

const fib = n => {
    const arr = [0, 1]
    let i = 1
    while(arr[arr.length - 1] <= n){
        arr.push(arr[arr.length - 2] + arr[arr.length - 1])
        i++
    }
    arr.pop()
    return arr
}

const sumOddFibonacciNumbers = num => fib(num).reduce((a, c) => Math.abs(c % 2) === 1 ? c + a : a, 0)

Day 10

const byTwo = a => {
    const r = []
    for(let i = 0; i < a.length; i++){
        if(i < a.length - 1 ){
            r.push([a[i], a[i + 1]])
        }
    }
    return r
}

const adjacentElementsProduct = nums => Math.max(...byTwo(nums).map(e => e[0] * e[1]))

Day 11

const avoidObstacles = nums => {
  let n = 2,
    found = false
  while (!found) {
    if (nums.every(num => num % n !== 0)) {
      found = true
    } else {
      n++
    }
  }
  return n
}

Day 12

const validTime = str => parseInt(str.split(":")[0], 10) >= 0 && parseInt(str.split(":")[0], 10) <= 24 && parseInt(str.split(":")[1], 10) >= 0 && parseInt(str.split(":")[1], 10) < 60 

// const validTime = str => ~~str.split(":")[0] >= 0 && ~~str.split(":")[0] <= 24 && ~~str.split(":")[1] >= 0 && ~~str.split(":")[1] < 60 

// In the 24-hour time notation, the day begins at midnight, 00:00, and the last minute of the day begins at 23:59. Where convenient, the notation 24:00 may also be used to refer to midnight at the end of a given date[5] — that is, 24:00 of one day is the same time as 00:00 of the following day.

Day 13

const extractEachKth = (nums, index) => nums.filter((_,i) => (i + 1) % index)

Day 14

const byTwo = a => {
    const r = []
    for(let i = 0; i < a.length; i++){
        if(i < a.length - 1 ){
            r.push([a[i], a[i + 1]])
        }
    }
    return r
}


const arrayMaximalAdjacentDifference = nums => Math.max(...byTwo(nums).map(a => Math.abs(a[0] - a[1])))

Day 15

This one took far too long, mainly because I got distracted by matrix transforms in CSS - horrible! Still, got in there just in time.

// javascript


const previous = document.querySelector(".previous")
const next = document.querySelector(".next")
const gallery = document.querySelector(".gallery")
const cards = Array.from(document.querySelectorAll(".card"))
const matrixR = /matrix\(\s*\d+,\s*\d+,\s*\d+,\s*(\-*\d+),\s*\d+,\s*\d+\)/

gallery.style.transform = window.getComputedStyle(gallery).transform

next.addEventListener('click', e => {
  let current = null
  cards.forEach((c, i) => {
    if(c.classList.contains('current')){
      current = i
    }
  })
  if(current < cards.length - 1){
    cards[current].classList.remove('current')
    cards[current + 1].classList.add('current')
    gallery.style.transform = `matrix(1, 0, 0, 1, ${-220 * (current + 1)}, 0)`
    previous.style.opacity = 1
    if(current + 1 === cards.length - 1){
      next.style.opacity = 0.3
    }
  }
})

previous.addEventListener('click', e => {
  let current = null
  cards.forEach((c, i) => {
    if(c.classList.contains('current')){
      current = i
    }
  })
  if(current > 0){
    cards[current].classList.remove('current')
    cards[current - 1].classList.add('current')
    gallery.style.transform = `matrix(1, 0, 0, 1, ${-220 * (current - 1)}, 0)`
    next.style.opacity = 1
    if(current - 1 === 0){
      previous.style.opacity = 0.3
    }
  }
})

Day 16

const insertDashes = arr => arr.split('').map((c, i, a) => c === ' ' ? ' ' : a[i+1] === ' ' ? c : i < a.length - 1 ? `${c}-` : c ).join('')

Day 17

const differentSymbolsNaive = str => new Set(str.split('')).size

The byTwo function I used in Days 10 and 14 above can be re-written like this:

[1,2,3,4,5,6].map((c, i, a) => a[i+1] && [c, a[i+1]]).filter(e => Array.isArray(e))

Much nicer, and means that Day 10 can be written like this:

const adjacentElementsProduct = nums => Math.max(...nums.map((c, i, a) => a[i+1] && [c, a[i+1]]).filter(e => Array.isArray(e)).map(e => e[0] * e[1]))

With Day 14 written like this:

const arrayMaximalAdjacentDifference = nums => Math.max(...nums.map((c, i, a) => a[i+1] && [c, a[i+1]]).filter(e => Array.isArray(e)).map(a => Math.abs(a[0] - a[1])))

Day 18

There was mention of a for loop in the description, but map seemed the most suited to this one. I'm really not sure why, but I soirt of get a kick out of using c,i,a as the arguments. c is the current array element, i is the index of that element in the (a) array. Works, doesn't it?

const arrayPreviousLess = nums => nums.map((c,i,a) => !i ? -1 : a[i-1] < c ? a[i-1] : -1)

Or shrunk:

const arrayPreviousLess=n=>n.map((c,i,a)=>!i?-1:a[i-1]<c?a[i-1]:-1)

After playing with it some more, I clocked that the byTwo refactor above was a bit pants. We return the appropriately chunked array, but only after removing the extraneous element with the filter - this seems inefficient to me. I got to thinking about using a reduce function instead but got into all sorts of issues trying to return the array after pushing the new element. I had problems because I'd forgotten that push returns the length of the array after the addition rather than the array with the new element. That's where concat came to the rescue, now we have:

[1,2,3,4,5,6].reduce((n,c,i,a)=>(a[i+1])?n.concat([[c,a[i+1]]]):n,[])

Much cleaner, but it messes up my c,i,a, especially as the first value should be another a, to represent the accumulator. Ah well, nothing's perfect I guess, and a,c,i,a won't work. Replacing the first a with n seems to work OK though.

Day 19

That was a fun one, mainly because I wanted to try alternative ways of solving it, and ways of solving it which didn't use charCodeAt or a Set.

const alphabetSubsequence = str => 
  str === str.split('')
             .reduce((arr, c) => 
               !!~arr.indexOf(c) 
                 ? arr 
                 : arr.concat([c]), [])
             .sort((a, b) => a.localeCompare(b))
             .join('')
const alphabetSubsequence = str => 
  str === [...new Set(str.split('')
                         .sort((a, b) => 
                           a > b 
                             ? 1 
                             : a < b 
                               ? -1 
                               : 0))]
                         .join('')
const alphabetSubsequence = str => 
  str === [...new Set(str.split('')
                         .sort((a, b) => 
                           a.localeCompare(b)
                         )
                     )
          ].join('')
const alphabetSubsequence = str => 
  str === [...new Set(str.split('')
                         .sort((a, b) => 
                           a.charCodeAt(0) - b.charCodeAt(0)
                         )
                     )
          ].join('')

I did learn an interesting thing though. Running this:

"abcdcba".split('').reduce((arr, c) => !!~arr.indexOf(c) ? arr : arr.concat([c]))

Produces the string "abcd". Whereas, runing this:

"abcdcba".split('').reduce((arr, c) => !!~arr.indexOf(c) ? arr : arr.concat([c]), [])

Gives us ["a", "b", "c", "d"]. This shouldn't be surprising really - I'd just forgotten about how indexOf and concat can both work on strings as well as arrays. Neat!

Day 20

// Using an Object literal:
function getTLD (tld) {
  const tlds = {
    'org': () => 'organization',
    'com': () => 'commercial',
    'net': () => 'network',
    'info': () => 'information',
    'default': () => 'unrecognised',
  };
  return (tlds[tld] || tlds['default'])();
}

// Using a plain Object:
// const tlds = {
//     org: 'organization',
//     com: 'commercial',
//     net: 'network',
//     info: 'information'
// }
// const domainType = ds => ds.map(d => tlds[d.split('.')[d.split('.').length - 1]])

const domainType = ds => ds.map(d => getTLD(d.split('.')[d.split('.').length - 1]))

I was going to go to town and list all the TLDs from here. Then I realsied I'd be doing that most of the day, so I just added a default unrecognised.

Day 21

Not big nor clever, too tired to be clever today!

const sumOfTwo = (nums1, nums2, value) => {
  for(let i = 0; i < nums1.length; i++){
    for(let j = 0; j < nums2.length; j++){
      if(nums1[i] + nums2[j] === value){
        return true
      }
    }
  }
  return false
}

The coffee kicked in:

const sumOfTwo=(a1,a2,t)=>!!~a1.map(e=>a2.map(a=>a+e)).flat().indexOf(t)

Perforamce wise, there's not much in it.

Day 22

const extractMatrixColumn = (matrix, column) => matrix.reduce((a, c) => a.concat(c[column]), [])

That was fun! Using the concat thing I mentioned above within a reduce, such a nice way of generating an array from another.

Day 23

Insomnia meant I saw the original solution via the email... I was going to do something similar so had to re-think things and this is what I came up with:

document.getElementById('btn').setAttribute('disabled', true)
document.getElementById('string').addEventListener('input', ev => {
  const remaining = 140 - ev.target.value.length
  document.getElementById('counterFooter').textContent = remaining + '/140'
  document.getElementById('counterFooter').style.color = ''
  document.getElementById('btn').removeAttribute('disabled')
  remaining < 0 && document.getElementById('btn').setAttribute('disabled', true)
  remaining === 140 && document.getElementById('btn').setAttribute('disabled', true)   
  remaining < 20 && (document.getElementById('counterFooter').style.color = 'red')
})
document.getElementById('btn').addEventListener('click', ev => {
  window.open(`https://twitter.com/intent/tweet?text=${encodeURI(document.getElementById('string').value)}&hashtags=JavaScriptmas&via=DThompsonDev`)
})

I also tweaked the CSS a smidge:

button{
  width:50%;
  background-color: rgb(29, 161, 242);
  border-radius: 10px;
  padding: 0 10%;
}
button h2{
    color: #fff;
}
button:disabled {
   opacity: .5;
   cursor: default;
}

Not a huge change I know, but I do prefer disabling things via an attribute rather than a class - it also means that the click event handler isn't fired on a button with the disabled attribute set; so less code! Oh, I almost forgot, it tweets too, and with the appropriate hashtag and target too! I've gone back to this and changed the keyup to input, helps a smidge, especially for odd times when input is added via the right-click mechanism.

One more to go, yippee!

Day 24

//EDIT THIS FUNCTION
const spin = async () => {
    //WRITE YOUR CODE HERE
    let counter = 0
    spinningElem.innerHTML = counter
    while(!pushed) {
        await sleep(75) //Paste this wherever you need to sleep the incrimentor 
        counter++
        spinningElem.innerHTML = counter
    }
    stop(counter); //Trigger this function when the STOP button has been pushed 
  
}

//EDIT THIS FUNCTION
function stop(i){
    //WRITE YOUR CODE HERE
    pushed = true
    var result = document.getElementById('result'); //display your result message here
    result.innerHTML = (i === targetInt) ? "Congratulations!" : `Too bad, you were out by ${Math.abs(targetInt - i)}, refresh the page to try again.`
}

Conclusion

Well, that was nice, the certificate for this post came though just now:

No comments:

Post a Comment