Tuesday 22 December 2020

Chase the Pizza!

This post is, basically, a collection of random things. The first is a chart from a Google Sheet documenting the distance I've walked over 2021 for the Walk 1000 miles in 2021 challenge. What makes that fun is the way the data is collected. Unfortunately, there's no integration with Polar - my fitness tracker of choice - so I have to enter the steps from the previous day each morning. But then something cool happens. I set up URLooker to act as a webhook for IFTTT, which then slings the data from My Virtual Mission into a Google Sheet. Within the Sheet, there's a script which fires every evening and mangles the data into proper values. The following is the relevant script, and if you can see something that could be improved, please tell me:

const addFormula = () => {
  const factor = 0.62137119
  const returnSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1')
  const rowCount = returnSheet.getLastRow()
  for (let i = rowCount; i > 0; i--) {
    if (returnSheet.getRange('C' + i).getValue() === 'Removed'){
      returnSheet.deleteRow(i);
    } else {
      if(!returnSheet.getRange('F' + i).getValue()){
        const rawValue = returnSheet.getRange('D' + i).getValue()
        const rawValueNumber = Number(rawValue.toString().replace(/\D/g, ""))
        const rawValueUnit = rawValue.toString().replace(/[0-9 ]+/g, "")
        returnSheet.getRange('F' + i).setValue(rawValueNumber)
        returnSheet.getRange('G' + i).setValue(rawValueUnit)
        returnSheet.getRange('H' + i).setValue(rawValueUnit === 'km' ? rawValueNumber * factor : rawValueNumber)
      }
    }
  }
}

This is a cool way of generating an avatar. The following is mine.

And this is my first try at making a game with MakeCode, which is jolly good fun!

Proper link here.

Sunday 20 December 2020

Type checking JavaScript

I read an article yesterday by Gil Tayar, which was fascinating. In it, he talked about his journey from strongly typed languages to untyped languages and back again. I'd recommend reading the article; it's well written and very informative.

His main gist seemed to be that TypeScript wasn't all that bad. I agree, and I've used it on and off for ages, I think I even have a certificate for a course somewhere, but I'm all for the flexibility of JavaScript.

Anyway, after reading it, I thought I'd give it a go, mostly as I wrote something a little while back which already implemented JSDocs. The process wasn't all that hard but was a bit of a faff. The odd wobble came from destructuring arguments in one of the constructors of one of the toy car child classes, and I was blown if I could understand the differences between String and string types until I read up some more. There were a few such quibbles, and TypeScript had issues with a few of my more relaxed approaches to JavaScript - such as explicitly checking for the existence of variables using !!. You can check out some of the other changes on a branch on GitHub.

I'm not sure I'll do it again, but it was worth spending a little while playing with it last night. It certainly would help in terms of distributing code to other developers, especially in terms of the automatic type checking offered by many IDEs and editors. I'm not overly sure of the strict setting - I like the whole fast and loose aspects of JavaScript quite a lot but, indeed, if I were to upload an NPM package I'd be inclined to be a little more rigorous.

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: