Saturday 15 July 2023

I’ve recently been updating a website and trying to implement a data of birth field; what do you think is the best input type for such a field?

<input type="date" id="dob" name="dob" />

I thought a date type input would be best, but then I tried entering the date of birth of a lady joining the club on my phone, and I gave up! I asked her to email it to me as it was getting embarrassing scrolling through all the months to the 1970s – see, she wasn’t even as old as me, and I thought about how annoying it is to have to scroll to enter my date of birth!

At least on Android, you must swipe through each month in each year, going back in time to that halcyon time when you were first spawned. 50 years of 12 months are 600 swipes through your history, and let me tell you, that gets depressing fast!

Anyway, I thought about how it’s managed elsewhere and found that the UK Government’s implementation (also used by the NHS) and the GOV.UK Design System has a lovely mechanism for date inputs with three separate fields (one for the day, one for the month and one for the year). It’s located here, but the page tends to break. The NHSs version doesn’t break, though – and they acknowledge its ancestry.

As a fan of web components, I thought I’d implement the same thing, add it to our website, and allow others to take advantage of it as well. I built-in date validation using the native date object rather than Moment.js or date-fns, as I’ve played before with using dropdown fields to enter the values in a Vue component in the dim and distant past – that was fun though as the dropdown values would only allow valid options to be selected. In that component, you could only choose leap years if you had selected the 29th of February, for instance.

What I was particularly pleased about was the ability to add native form validation, which is something I’ve never tried before but was quite easy to implement. I’ve utilised significant manual testing and asked a mate to do some Selenium testing – but unit testing within web components seems to be something of a dark art, especially if not using a framework to create the component – and why would you use a framework if you’re avoiding frameworks and writing native web components?

There’s a CODEPEN to play with here, and it’s on npm so that you can play with it too – if you spot anything, please let me know; hey, tell me your thoughts anyway! I’ve used it within a React application, and it also seems to work well there.

Saturday 11 February 2023

GitHub Contributions since 2011

Friday 27 January 2023

Smart meter? Snitch meter!

Last night I was woken by a thought - well, that's not correct; I was mainly woken by pins and needles caused by the dog sleeping on my feet! As I wriggled my toes and tried to get the blood flowing again (while ignoring the growls from the dog), I was struck by the thought of energy prices and how they were likely to fall. Wholesale energy prices are falling - some would say plummetting - so I'm guessing that OFGEM will lower the cap sooner rather than later.

Alongside the appreciation that the currently dire situation is likely to change, I remembered something mentioned when I was studying. An old professor asked us to consider how much of our utility bill comprised the cost of the mechanisms employed by calculating our bill. He mentioned that once telephone lines are installed, the outgoings for telephony companies are maintenance and installation of new lines and staff. As such, everything but a small portion of our bills was free and clear profit after paying off the initial investment in infrastructure. He suggested that roughly half of the phone bill was to cover the generation of the bills themselves: the recording of call duration and destination and the postage of those bills to us. Of course, I'm guessing that's changed in these times of online accounts, with that portion of the bill significantly reduced. Have these efficiencies made their way to us as decreasing bills?

I'm reminded of the boon that social media is to the intelligence services - instead of dedicating personnel to track our movements; we do it ourselves.

This reduction was probably the same for the other utilities, such as gas or electricity (let's pop them under the umbrella of energy utilities). I am trying to remember the last time we had any of our meters read - but it was certainly a fair few years ago - thus, the staffing costs have been reduced. This reduction in the number of meter readers has been thanks to smart meters and consumers providing their own readings.

But that's odd, isn't it? Instead of someone coming each quarter to check how much energy has been used in a property and a bill generated from that figure, we now provide that data monthly - or more frequently when we have a smart meter. Now we can tell how much energy we've used and be charged the going rate for that energy at that particular time. December is cold, and the energy price rises; we'll pay more. January is even colder, but energy prices have dropped - do we pay less...? The delay in OFGEM dropping their cap means that we don't - though the utility companies are paying less for the energy they provide.

So what to do? I'm pondering how well I should report my energy consumption. If the cap is high - is it better to report reduced usage as a consumer that provides their readings...? When the cap is lower, I can give increased figures to get me back up to the actual amount. Presumably, that would be far better than relying on the estimated figures used by energy companies - at least for me as a consumer. It does smack of being a gamble, though; who is to say when prices will fall?

Friday 13 January 2023

TopTracker total hours snippet

I've been using TopTracker for years to keep track of my hours worked. I've recently clocked that I end up firing up a calculator to check how many hours I've worked for a given week; this seemed a little bit silly, TBH, so this morning I created a quick snippet. 5 minutes work now, to save lots of work later seems like a fair trade:

Thursday 1 December 2022

Scrimba's JavaScriptmas and Advent of Code 2022

Day 1

JavaScriptmas

const panic = str => `${str.toUpperCase().split(' ').join(' 😱 ')}!`
console.log(panic("I'm almost out of coffee"))
console.log(panic("winter is coming"))

Advent of Code

const elves = input.split('\n\n').map(e => Number(e.split('\n').reduce((a, c) => Number(a) + Number(c))))
console.log(Math.max(...elves))
elves.sort((a, b) => b - a)
console.log(elves.slice(0, 3).reduce((a, c) => a + c))

Day 2

JavaScriptmas

const transformData = d => d.map(e => ({
    fullName: `${e.name.first} ${e.name.last}`,
    birthday: new Date(e.dob.date).toDateString()
}))

Advent of Code

/**
 * Rock:     A, X. Worth: 1. Beats: Scissors
 * Paper:    B, Y. Worth: 2. Beats: Rock
 * Scissors: C, Z. Worth: 3. Beats: Paper
 * 0 if you lost,
 * 3 if the round was a draw
 * and 6 if you won
 */ 

console.log(input.split('\n').reduce((a, c) => {
  const [them, me] = c.split(' ')
  if(them === 'A'){
    if(me === 'X'){
      a += 3 + 1
    }
    if(me === 'Y'){
      a += 6 + 2
    }
    if(me === 'Z'){
      a += 0 + 3
    }
  }
  if(them === 'B'){
    if(me === 'X'){
      a += 0 + 1
    }
    if(me === 'Y'){
      a += 3 + 2
    }
    if(me === 'Z'){
      a += 6 + 3
    }
  }
  if(them === 'C'){
    if(me === 'X'){
      a += 6 + 1
    }
    if(me === 'Y'){
      a += 0 + 2
    }
    if(me === 'Z'){
      a += 3 + 3
    }
  }
  return a
}, 0))

/**
 * Rock:     A, X. Worth: 1. Beats: Scissors
 * Paper:    B, Y. Worth: 2. Beats: Rock
 * Scissors: C, Z. Worth: 3. Beats: Paper
 * 0 if you lost,
 * 3 if the round was a draw
 * and 6 if you won
 * X: Lose
 * Y: Draw
 * Z: Win
 */ 

console.log(input.split('\n').reduce((a, c) => {
  const [them, me] = c.split(' ')
  if(them === 'A'){ // They've played Rock
    if(me === 'X'){ // I need to lose
      a += 0 + 3    // I play Scissors
    }
    if(me === 'Y'){ // I need to draw
      a += 3 + 1    // I play Rock
    }
    if(me === 'Z'){ // I need to win
      a += 6 + 2    // I play Paper
    }
  }
  if(them === 'B'){ // They've played Paper
    if(me === 'X'){ // I need to lose
      a += 0 + 1    // I play Rock
    }
    if(me === 'Y'){ // I need to draw
      a += 3 + 2    // I play Paper
    }
    if(me === 'Z'){ // I need to win
      a += 6 + 3    // I play Scissors
    }
  }
  if(them === 'C'){ // They've played Scissors
    if(me === 'X'){ // I need to lose
      a += 0 + 2    // I play Paper  
    }
    if(me === 'Y'){ // I need to draw
      a += 3 + 3    // I play Scissors  
    }
    if(me === 'Z'){ // I need to win
      a += 6 + 1    // I play Rock  
    }
  }
  return a
}, 0))

Day 3

JavaScriptmas

const faveFoods = {
    breakfast: 'croissants',
    lunch: 'pasta',
    supper: 'pizza'
}

const {breakfast, lunch, supper} = faveFoods

document.getElementById('meals').innerHTML = `
For breakfast, I only like ${breakfast}. For lunch, I love ${lunch}, and for supper I want usually want ${supper}.`

Advent of Code

// drop 96 for lowercase
// drop 38 for uppercase

const getCodeValue = letter => letter === letter.toLowerCase() 
  ? letter.charCodeAt(0) - 96
  : letter.charCodeAt(0) - 38

const getIntersection = (a, b) => {
  for(let i in a){
    if(b.includes(a[i])){
      return a[i]
    }
  }
}

const getIntersectionThree = (a, b, c) => {
  for(let i in a){
    if(b.includes(a[i]) && c.includes(a[i])){
      return a[i]
    }
  }
}

console.log(input.split('\n').reduce((a, c) => a + getCodeValue(getIntersection(...[c.slice(0, Math.ceil(c.length / 2)).split(''), c.slice(Math.ceil(c.length / 2)).split('')]))
, 0))

let total = 0
const lines = input.split('\n')
for (let i = 0; i < lines.length; i += 3) {
  const chunk = lines.slice(i, i + 3);
  total += getCodeValue(getIntersectionThree(...chunk))
}
console.log(total)

console.log(input.split('\n').reduce((p, _, i, a) => !(i%3) 
  ? p + getCodeValue(getIntersectionThree(...a.slice(i, i + 3))) 
  : p, 0))

Day 4

JavaScriptmas

const whisper = s => `shh... ${s.endsWith('!') 
  ? s.toLowerCase().slice(0, -1) 
  : s.toLowerCase()}`

Advent of Code

const range = (start, stop) => Array.from({ length: stop - start + 1 }, (_, i) => start + i)
const contains = (a, b) => a.every(e => b.includes(e)) || b.every(e => a.includes(e))
const overlaps = (a, b) => a.some(e => b.includes(e))
                                                                  
console.log(input.split('\n').reduce((a, line) => {
  const [firstArea, secondArea] = line.split(',').map(elf => range(...elf.split('-').map(e => Number(e))))
  a.contains += contains(firstArea, secondArea) ? 1 : 0
  a.overlaps += overlaps(firstArea, secondArea) ? 1 : 0
  return a
}, {
  contains: 0,
  overlaps: 0
}))

// let containedWithin = 0
// let overlaps = 0

// input.split('\n').forEach(line => {
//   const [firstArea, secondArea] = line.split(',').map(elf => range(...elf.split('-').map(e => Number(e))))
//   if(secondArea.every(e => firstArea.includes(e)) || firstArea.every(e => secondArea.includes(e))){
//     containedWithin += 1
//   }
//   if(secondArea.some(e => firstArea.includes(e))){
//     overlaps += 1
//   }
// })
// console.log(containedWithin, overlaps)

Day 5

JavaScriptmas

const getSaleItems = d => d.filter(e => e.type === "sweet").map(e => ({item: e.item, price: e.price}))

Advent of Code

const chunk = (str, size) => {
  const numChunks = Math.ceil(str.length / size)
  const chunks = new Array(numChunks)
  for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
    chunks[i] = str.substr(o, size)
  }
  return chunks
}

const [initialState, instructions] = input.split('\n\n')
const initialStateLines = initialState.split('\n').reverse()
const positions = chunk(initialStateLines.shift(), 4).reduce((a, c) => {
  a[Number(c.trim())] = []
  return a
}, {})

initialStateLines.forEach(line => {
  chunk(line, 4).forEach((position, index) => {
    if (position.trim().length) {
      const { groups: { letter } } = /\s*\[(?<letter>[A-Z])\]\s*/.exec(position)
      positions[Object.keys(positions)[index]].push(letter)
    }
  })
})

/**
 * Part 1
 */
// instructions.split('\n').forEach(instruction => {
//   const {groups: {num, from, to}} = /move (?<num>[0-9]+) from (?<from>[0-9]+) to (?<to>[0-9]+)/.exec(instruction)
//   for(let i = 0; i < Number(num); i++){
//     positions[Number(to)].push(positions[Number(from)].pop())
//   }
// })

/**
 * Part 2
 */
instructions.split('\n').forEach(instruction => {
  const { groups: { num, from, to } } = /move (?<num>[0-9]+) from (?<from>[0-9]+) to (?<to>[0-9]+)/.exec(instruction)
  positions[Number(to)].push(...positions[Number(from)].slice(-Math.abs(Number(num))))
  positions[Number(from)].splice(positions[Number(from)].length - Number(num), Number(num) )
})

console.log(Object.keys(positions).reduce((a, c, i) => {
  return a + positions[c].at(-1)
}, ''))

Day 6

JavaScriptmas

const getRandomNumberOfTacos = () => new Array(Math.floor(Math.random() * (10 - 1) + 1)).fill('🌮')

Advent of Code

const test_input_1 = `mjqjpqmgbljsphdztnvjfqwrcgsmlb`
const test_input_2 = `bvwbjplbgvbhsrlpgdmjqwftvncz`
const test_input_3 = `nppdvjthqldpwncqszvftbrmjlhg`
const test_input_4 = `nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg`
const test_input_5 = `zcfzfwzzqfrljwzlrfnpqdbhtmscgvjw`

const findPacket = stream => {
  for(let i = 3; i <= stream.length; i++){
    if(new Set(Array.from({length: 4}, (_, a) => stream.charAt(i - a))).size === 4){
      return i + 1
    }
  }
}

const findMessage = stream => {
  for(let i = 13; i <= stream.length; i++){
    if(new Set(Array.from({length: 14}, (_, a) => stream.charAt(i - a))).size === 14){
      return i + 1
    }
  }
}

console.log(findPacket(test_input_1))
console.log(findPacket(test_input_2))
console.log(findPacket(test_input_3))
console.log(findPacket(test_input_4))
console.log(findPacket(test_input_5))
console.log(findPacket(input))

console.log(findMessage(test_input_1))
console.log(findMessage(test_input_2))
console.log(findMessage(test_input_3))
console.log(findMessage(test_input_4))
console.log(findMessage(test_input_5))
console.log(findMessage(input))

Day 7

JavaScriptmas

const altCaps = str => str.split('').map((c, i) => i % 2 ? c : str.charCodeAt(i) > 96 || str.charCodeAt(i) < 123 ? c.toUpperCase() : c).join('')

Advent of Code

// import { Tree } from './Tree.js'
// import { TreeNode } from './TreeNode.js'

class TreeNode {
  constructor(key, value = key, parent = null) {
    this.key = key;
    this.value = value;
    this.parent = parent;
    this.children = [];
  }

  get isLeaf() {
    return this.children.length === 0;
  }

  get hasChildren() {
    return !this.isLeaf;
  }
}

class Tree {
  constructor(key, value = key) {
    this.root = new TreeNode(key, value);
  }

  *preOrderTraversal(node = this.root) {
    yield node;
    if (node.children.length) {
      for (let child of node.children) {
        yield* this.preOrderTraversal(child);
      }
    }
  }

  *postOrderTraversal(node = this.root) {
    if (node.children.length) {
      for (let child of node.children) {
        yield* this.postOrderTraversal(child);
      }
    }
    yield node;
  }

  insert(parentNodeKey, key, value = key) {
    for (let node of this.preOrderTraversal()) {
      if (node.key === parentNodeKey) {
        node.children.push(new TreeNode(key, value, node));
        return true;
      }
    }
    return false;
  }

  remove(key) {
    for (let node of this.preOrderTraversal()) {
      const filtered = node.children.filter(c => c.key !== key);
      if (filtered.length !== node.children.length) {
        node.children = filtered;
        return true;
      }
    }
    return false;
  }

  find(key) {
    for (let node of this.preOrderTraversal()) {
      if (node.key === key) return node;
    }
    return undefined;
  }
}

/*
 * https://www.30secondsofcode.org/articles/s/js-data-structures-tree
 */

addEventListener('DOMContentLoaded', async () => {
  //const res = await fetch("./test.txt")
  const res = await fetch("./input.txt")
  const text = await res.text()
  const input = text.split('\n')
  let target = null
  let tree = null
  let lineParts
  input.forEach((line, index) => {
    if (!index) {
      tree = new Tree('/', 0)
      target = tree.root
    } else {
      if (line.charAt(0) === '$') {
        lineParts = line.split(' ')
        if (lineParts.length === 3) {
          if (lineParts[2] === '..') {
            target = target.parent
          } else if (lineParts[2] === '/') {
            target = tree.root
          } else {
            target = target.children.find(ch => ch.key === lineParts[2])
          }
        }
      } else {
        lineParts = line.split(' ')
        if (lineParts[0] === 'dir') {
          target.children.push(new TreeNode(lineParts[1], 0, target))
        } else {
          const value = Number(lineParts[0])
          target.children.push(new TreeNode(lineParts[1], value, target))
          target.value += value
          let parent = target.parent
          while (parent) {
            parent.value += value
            parent = parent.parent
          }
        }
      }
    }
  })

  const dirs = [...tree.postOrderTraversal()].filter(x => x.hasChildren)
  console.log('Part 1:', dirs.reduce((a, c) => {
    if (c.value <= 100000) {
      a += c.value
    }
    return a
  }, 0))

  const spaceRequired = 30000000 - (70000000 - tree.root.value)
  console.log('Space required: ', spaceRequired)
  console.log('Part 2:', dirs.filter(dir => {
    if (dir.value >= spaceRequired) {
      console.log(dir.key, dir.value)
      return true
    }
    return false
  }).sort((a, b) => a.value - b.value)[0].value)
})

Day 8

JavaScriptmas

const validTime = str => {
  const [hours, minutes] = str.split(':').map(n => Number(n))
  return hours >= 1 && hours <= 24 && minutes >= 0 && minutes < 60
}

Day 9

JavaScriptmas

const capitalizeWord = w => `${w.charAt(0).toUpperCase()}${w.slice(1, w.length)}`

const toTitleCase = s => s.split(' ').map(w => capitalizeWord(w)).join(' ')

Day 10

JavaScriptmas

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

Day 11

JavaScriptmas

const reverseString = arr => arr.split('').reverse().join('')

const reverseStringsInArray = arr => arr.map(a => reverseString(a))

Day 12

JavaScriptmas

class MenuItem extends HTMLElement{
    static get observedAttributes() {
        return [
            'food'
        ]
    }
    constructor() {
        super()
    }
    render() {
        this.innerHTML = this.html
    }
    get html() {
        return `<div class="food">${this.food}</div>`
    }
    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue !== newValue) {
            this.render()
        }
    }
    set food(v){
        this.setAttribute('food', v)
    }
    get food() {
        return this.getAttribute('food')
    }
}
window.customElements.define('menu-item', MenuItem)
const menu = document.getElementById('menu')
const dinnerFoods = ['🍝','🍔','🌮']
menu.innerHTML = dinnerFoods.map(food => `<menu-item food="${food}"/></menu-item>`).join('')

Day 13

JavaScriptmas

const emojifyWord = w => w.charAt(0) === ':' && w.charAt(w.length - 1) === ':' ? emojis[w.slice(1, -1)] ? emojis[w.slice(1, -1)] : w.slice(1, -1) : w

const emojifyPhrase = phrase => phrase.split(' ').map(word => emojifyWord(word)).join(' ')

Day 14

JavaScriptmas

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

Day 15

JavaScriptmas

const isPalindrome = str => str === str.split('').reverse().join('')

Day 16

JavaScriptmas

const insertDashes = arr => arr.split(' ').map(w => w.split('').join('-')).join(' ')

Day 17

JavaScriptmas

const flatten = arr => arr.flat()
const flatten = arr => Array.prototype.concat.apply([], arr)
const flatten = arr => arr.toString().split(',')

Day 18

JavaScriptmas

const candies = (ch, ca) => Math.floor(ca/ch) * ch

Day 19

JavaScriptmas

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

Day 20

JavaScriptmas

const getFreePodcasts = data => podcasts.filter(p => !p.paid).map(({title, rating, paid}) => ({title, rating, paid}))

Day 21

JavaScriptmas

const awardBonuses = num => Array.from({length: num}, (_, i) => i + 1).forEach(i => console.log(`${i} - ${!(i%3) && !(i%5) ? 'JACKPOT! 1 Million and a Yacht!' : !(i%3) ? 'Vacation!' : !(i%5) ? '$100,000 bonus!' : ':('}`))

awardBonuses(100)

Day 22

JavaScriptmas

function getReadyTables() {
  const readyTables = []
  for (let i = 0; i < 2; i++) {
    readyTables.push(Math.round(Math.random()*20) + 1)
  }
  return readyTables
}

const displayTables = () => getReadyTables().map(el => {
  const table =  document.createElement('div')
  table.classList.add('table')
  table.innerText = el
  return table
})

document.getElementById('tables').append(...displayTables())

Day 23

JavaScriptmas

const sortProducts = data => data.sort((a, b) => a.price - b.price)

Day 24

JavaScriptmas

const player = document.getElementById("player")
const playSong = id => player.src = `https://www.youtube.com/embed/${id}?autoplay=1`

Tuesday 15 November 2022

Create dot notation array of strings from an Object

Last night, I created an object from an array of strings with dot notation; I needed to do that because I had previously flattened an object into those strings. I did this long and laboriously, but I asked a colleague to see if he could figure out how to do it using recursion. He did, and I'd like to share it here.

If we have this object:

{
  "thingOne": {
    "thingTwo": {
      "thingThree": true,
      "thingFour": true
    }
  },
  "thingyOne": {
    "thingyTwo": {
      "thingyThree": true,
      "thingyFour": true
    }
  }
}

Then this code:

(() => {

  const obj = {
    "thingOne": {
      "thingTwo": {
        "thingThree": true,
        "thingFour": true
      }
    },
    "thingyOne": {
      "thingyTwo": {
        "thingyThree": true,
        "thingyFour": true
      }
    }
  }

  const getObjStringsArr = (o = {}, arr = [], name = '') => {
    Object.keys(o).forEach(key => {
      if (o[key] === true) {
        arr.push(`${name}${key}`)
      } else {
        const nested = getObjStringsArr(o[key], arr, `${name}${key}.`)
        arr.concat(nested)
      }
    });
    return arr
  }

  console.log(getObjStringsArr(obj))

})()

It's brilliant having colleagues; it's even better having colleagues with huge brains! Thanks, Hamish!

It's not necessarily faster than my cludge, but it is far more elegant!

Monday 14 November 2022

Create an Object from dot notation

This was puzzling me all afternoon, and I came up with a bloody terrible work-around using eval, this evening I decided to do a little more research and found this answer on StackOverflow.

I've adapted it and it seems to work a treat:

(() => {

  const obj = {}

  const expand = (output, mystr, value) => {
    const items = mystr.split(".") // split on dot notation
    let ref = output // keep a reference of the new object
    //  loop through all nodes, except the last one
    for (let i = 0; i < items.length - 1; i++) {
      if (!ref[items[i]]) {
        // create a new element inside the reference
        ref[items[i]] = {}
      }
      ref = ref[items[i]] // shift the reference to the newly created object 
    }
    ref[items[items.length - 1]] = value // apply the final value
    return output // return the full object
  }

  const arr = [
    "thingOne.thingTwo.thingThree",
    "thingOne.thingTwo.thingFour",
    "thingyOne.thingyTwo.thingyThree",
    "thingyOne.thingyTwo.thingyFour"
  ]

  arr.forEach(a => {
    expand(obj, a, true)
  })
  
  console.log(obj)

})()

I was nearly there on my tod TBH, but wussed out at the end, thisis lovely though! It produces this lovely object:

{
  "thingOne": {
    "thingTwo": {
      "thingThree": true,
      "thingFour": true
    }
  },
  "thingyOne": {
    "thingyTwo": {
      "thingyThree": true,
      "thingyFour": true
    }
  }
}