Saturday 31 August 2024

AskJS Toy problem

There was a post on Reddit last week that got me sort of excited (I probably need to get out more).

It's been removed now, but my RSS feed still has it:

Write a function that takes a string representing open hours and returns an object where the keys are days of the week and the values are the corresponding open hours. Missing days should still be represented as a key but with an empty string value.

Examples:

"Mon-Sun 11:00 am - 10 pm" would return:

{
  Mon: "11:00 am - 10:00 pm", 
  Tues: "11:00 am - 10:00 pm", 
  Wed: "11:00 am - 10:00 pm", 
  Thu: "11:00 am - 10:00 pm", 
  Fri: "11:00 am - 10:00 pm", 
  Sat: "11:00 am - 10:00 pm", 
  Sun: "11:00 am - 10:00 pm"
}

"Mon-Thu, Sat 11 am - 9 pm / Fri 5 pm - 9 pm" would return:

{
  Mon: "11:00 am - 9:00 pm",
  Tues: "11:00 am - 9:00 pm",
  Wed: "11:00 am - 9:00 pm",
  Thu: "11:00 am - 9:00 pm",
  Fri: "5:00 pm - 9:00 pm",
  Sat: 11:00 am - 9:00 pm",
  Sun: ""
}

Additional test cases:

  • "Mon-Sun 11 am - 12 am"
  • "Mon-Fri, Sat 11 am - 12 pm / Sun 11 am - 10 pm"
  • "Mon-Thu, Sun 11:30 am - 10 pm / Fri-Sat 11:30 am - 11 pm"
  • "Mon-Thu 11 am - 11 pm / Fri-Sat 11 am - 12:30 am / Sun 10 am - 11 pm"

This is what I came up with:

/*
 * Names regex groups are really handy! Remember to end a regex with a ";"!
 */
const timeRegex = /(?<sHour>\d{1,2})(\:(?<sMinute>\d{1,2}))?\s(?<sAmPm>am|pm)\s\-\s(?<eHour>\d{1,2})(\:(?<eMinute>\d{1,2}))?\s(?<eAmPm>am|pm)/;
const daysRegex = /\b([a-zA-Z]{3,4}\-?)\b/g;
/*
 * Our tests:
 */
const tests = [
  "Mon-Sun 11:00 am - 10 pm",
  "Mon-Sun 11 am - 12 am",
  "Mon-Thu, Sat 11 am - 9 pm / Fri 5 pm - 9 pm",
  "Mon-Fri, Sat 11 am - 12 pm / Sun 11 am - 10 pm",
  "Mon-Thu, Sun 11:30 am - 10 pm / Fri-Sat 11:30 am - 11 pm",
  "Mon-Thu 11 am - 11 pm / Fri-Sat 11 am - 12:30 am / Sun 10 am - 11 pm"
]
/*
 * Our original object
 */
const daysObj = { "Mon": "", "Tues": "", "Wed": "", "Thu": "", "Fri": "", "Sat": "", "Sun": "" }
/*
 * Our array of days from the object above
 */
const days = Object.keys(daysObj)
/*
 * Our function to format a time
 */
const formatTime = (hour, minute, AmPm) => `${hour}:${minute} ${AmPm}`
/*
 * Our function to format a time duration
 */
const generateTime = (sHour, sMinute, sAmPm, eHour, eMinute, eAmPm) => {
  const sTime = formatTime(sHour, sMinute, sAmPm)
  const eTime = formatTime(eHour, eMinute, eAmPm)
  return `${sTime} - ${eTime}`
}
/*
 * Iterate over the tests
 */
for (const test of tests) {
  /*
   * Start with a fresh version of the days object by copying the one from above.
   */
  const returnObj = Object.assign({}, daysObj)
  /*
   * First we need to chunks of the string seperated by " / "
   * We'll then iterate over each chunk.
   */
  for (chunk of test.split(" / ")) {
    /*
     * We'll then get all the days from the chunk.
     */
    const daysResult = chunk.match(daysRegex)
    /*
     * We'll then destructure the groups from the regex match. Named regex groups are really helpful for this as we
     * we can then access the values by name from the groups object, and have default values for those that might be 
     * undefined.
     */
    const { sHour, sMinute = '00', sAmPm, eHour, eMinute = '00', eAmPm } = timeRegex.exec(chunk.trim()).groups
    /*
     * We'll then generate the time from the values we've extracted from the regex match.
     */
    const timeString = generateTime(sHour, sMinute, sAmPm, eHour, eMinute, eAmPm)
    /*
     * We'll then iterate over the days we've extracted from the chunk.
     */
    for (let i = 0; i < daysResult.length; i++) {
      /*
       * If the last character of the day is a hyphen, we know we need to iterate over the days array we created above.
       */
      if (daysResult[i].charAt(daysResult[i].length - 1) === "-") {
        /*
         * We'll then iterate over the days array we created above and populate it with the time string we've generated.
         */
        for (let l = days.findIndex(e => e === daysResult[i].slice(0, -1)); l < days.length; l++) {
          if (days[l] !== daysResult[i + 1]) {
            returnObj[days[l]] = timeString
          } else {
            /*
             * If the current day isn't the same as the next day in the array, we'll break.
             */
            break;
          }
        }
      } else {
        /*
         * If the last character of the day is not a hyphen, we can just set taht day with the time string we've 
         * generated.
         */
        returnObj[daysResult[i]] = timeString
      }
    }
  }
  /*
   * Display the result alng with the test
   */
  console.log(test, JSON.stringify(returnObj, null, 2))
}

That was fun!

Notes:

Be aware that there's no error checking... because that wasn't in the requirements, though it should be easy enough to add. For instance, we need to consider:

  • In a range of days does the second have an index before the first?
  • Is the start time after the end time?
  • What should happen with duplicate days, such that an date range is then partially or totally replicated within another range, or a single day exists within a previous range?

Monday 26 August 2024

Practicality over Purity

It was Voltaire who said that perfect is the enemy of good; it was me in many interviews who answered, "I'm a perfectionist", whenever anyone asked if I had a weakness. That's not strictly true; it's just something I read years ago about what to answer when asked that question... to be honest, it's good advice and a darn sight better than saying, "Benign dictatorship" when asked about my management style (I still think I would have been brilliant in that role too).

Anyway, it wasn’t precisely Voltaire’s aphorism that prompted me to write this; it was more along the lines of grokking that sometimes, being a purist can get in the way of making a useful thing. Let me explain a little more. I’ve been playing with Web Components for a long time, long before they became as popular as they seem to be now, and whenever I’ve created them, I’ve been conscious that the best place for them would be on npm. Once on npm, they can be imported using skypack or unpkg and used wherever without downloading and hosting them; they should just work (Indeed, whenever I demo them on codepen, that’s what I do to check the mechanism works).

To ensure that as many people find them helpful as possible, I try to make them as perfect as possible, anticipate where they might be used, and make them as flexible as possible. Even in the case of input elements, I try to make them form-associated (though that’s been a massive issue in the past—thanks to a dearth of information on making elements form-associated). This has stopped me from creating and using them in a more bespoke manner up until recently.

Recently, I’ve been involved as a subject-matter-expert (due to suffering a specific condition) with consulting and testing a research tool. I was provided with the underlying questionnaire to be used in that research. We were informed that a development team had been tasked with converting that questionnaire into an online tool which would record answers over days, weeks and months, and I thought that would be a fun way of filling a weekend – to try converting it myself. I’d also been reading about Beer CSS, which aims to translate a modern UI into an HTML semantic standard, which also sounded like a fun tool to play with.

As I started developing the application, I noticed that there would be many repeated code blocks. Each daily question was repeated six times, and the weekly question was repeated fourteen times. Once I started coding it, I noticed that the only difference between the questions was the specific language used and the options. Each question had a radio button to click for the value appropriate to the respondent. This was a perfect place to use a slot within a web component!

I started with the weekly question and copied the markup I’d already implemented:

<article class="large-padding">
  <i>lock_open</i>
  <p>
    <slot></slot>
  </p>
  <div class="grid d-grid-10">
    <div class="col s10 m5 l2">
      <label class="radio">
        <input type="radio"
               name="Domain-2-1"
               value="0">
        <span class="bold">None</span>
      </label>
    </div>
    <div class="col s10 m5 l2">
      <label class="radio">
        <input type="radio"
               name="Domain-2-1"
               value="1">
        <span class="bold">A little bit</span>
      </label>
    </div>
    <div class="col s10 m5 l2">
      <label class="radio">
        <input type="radio"
               name="Domain-2-1"
               value="2">
        <span class="bold">Moderately</span>
      </label>
    </div>
    <div class="col s10 m5 l2">
      <label class="radio">
        <input type="radio"
               name="Domain-2-1"
               value="3">
        <span class="bold">Quite a bit</span>
      </label>
    </div>
    <div class="col s10 m5 l2">
      <label class="radio">
        <input type="radio"
               name="Domain-2-1"
               value="4">
        <span class="bold">Extreme</span>
      </label>
    </div>
  </div>
</article>

You’ll no doubt appreciate the sheer amount of copying and pasting to get fourteen of these on the page simultaneously, but this translated into the following template literal within the component:

<article class="large-padding">
  <i${this.#disabled ? ' class="tertiary-text"' : ""}>
    ${this.#locked || this.#disabled ? "lock" : "lock_open"}
  </i>
  <p><slot></slot></p>
  <div class="grid d-grid-10">
    ${this.labels.map((label, i) => `
      <div class="col s10 m5 l2">
        <label class="radio">
          <input type="radio"
                 name="${this.name}"
                 value="${i}"
                 ${this.#value === i ? " checked" : ""}
                 ${this.#locked || this.#disabled ? " disabled" : ""} />
          <span class="bold">${label}</span>
        </label>
      </div>
    `).join("")}
  </div>
</article>

It was much neater, especially as the constructor had the values hard coded:

this.labels = [
  "None",
  "A little bit",
  "Moderately",
  "Quite a bit",
  "Extreme",
];

The keen-eyed amongst you will notice that we have several private values, namely #value, #locked, and #disabled. That’s not to say we don’t expose these values; we can set the value, locked, and disabled attributes, which will update the private values using getters and setters. Further, as we’ve defined the component as being form-associated, when we set the attribute from inside the element, the containing form can be notified of the change by dispatching a change event.

This is the complete code (as always, I’m more than happy to have input into how it might be improved):

import { v4 as uuidv4 } from "https://cdn.skypack.dev/uuid";

class WCEasyQ extends HTMLElement {
  #value = null;
  #locked;
  #disabled;

  static get observedAttributes() {
    return ["value", "name", "locked", "disabled"];
  }
  static formAssociated = true;

  constructor() {
    super();
    this.labels = [
      "None",
      "A little bit",
      "Moderately",
      "Quite a bit",
      "Extreme",
    ];
    this.internals = this.attachInternals();
    this.shadow = this.attachShadow({
      mode: "closed",
      delegatesFocus: true,
    });
    this.name = uuidv4();
  }

  get css() {
    return `
      <style>
        @import url("https://cdn.jsdelivr.net/npm/beercss@3.6.0/dist/cdn/beer.min.css");
        .d-grid-10 {
          margin-block-start: 1rem;
          ---gap: 1rem;
          display: grid;
          grid-template-columns: repeat(
            10,
            calc(10% - var(---gap) + (var(---gap) / 10))
          );
          gap: var(---gap);
        }
        article {
          & i {
            &.tertiary-text {
              cursor: not-allowed !important;
            }
            &:first-child {
              position: absolute;
              top: 10px;
              right: 10px;
              cursor: pointer;
            }
          }
        }
      </style>
    `;
  }

  get html() {
    return `
      <article class="large-padding">
        <i${this.#disabled ? ' class="tertiary-text"' : ""}>
          ${this.#locked || this.#disabled ? "lock" : "lock_open"}
        </i>
        <p><slot></slot></p>
        <div class="grid d-grid-10">
          ${this.labels.map((label, i) => `
            <div class="col s10 m5 l2">
              <label class="radio">
                <input type="radio"
                       name="${this.name}"
                       value="${i}"
                       ${this.#value === i ? " checked" : ""}
                       ${this.#locked || this.#disabled ? " disabled" : ""} />
                <span class="bold">${label}</span>
              </label>
            </div>
          `).join("")}
        </div>
      </article>
    `;
  }

  set value(value) {
    if (value !== null) {
      this.setAttribute("value", Number(value));
      this.#value = Number(value);
    } else {
      this.removeAttribute("value");
      this.#value = null;
    }
    this.internals.setFormValue(this.#value);
  }

  get value() {
    this.#value =
      this.hasAttribute("value") && this.getAttribute("value") !== null
        ? Number(this.getAttribute("value"))
        : null;
    return this.#value;
  }

  set name(value) {
    this.setAttribute("name", this.name);
  }

  get name() {
    return this.hasAttribute("name") && this.getAttribute("name") !== null
      ? this.getAttribute("name")
      : this.name;
  }

  set locked(value) {
    this.#locked = value;
    this.render();
  }

  get locked() {
    this.#locked = this.hasAttribute("locked");
    return this.#locked;
  }

  set disabled(value) {}

  get disabled() {
    this.#disabled = this.hasAttribute("disabled");
    return this.#disabled;
  }

  handleDisabled(value) {
    this.render();
  }

  render() {
    if (this.shadow) {
      this.shadow.removeEventListener("change", this.handleChange);
    }
    if (this.icon) {
      this.icon.removeEventListener("click", this.handleClick);
    }
    this.shadow.innerHTML = `${this.css}${this.html}`;
    this.icon = this.shadow.querySelector("i");
    this.inputs = this.shadow.querySelectorAll("input");
    this.shadow.addEventListener("change", this.handleChange.bind(this));
    this.icon.addEventListener("click", this.handleClick.bind(this));
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (name === "locked") {
        this.#locked = this.hasAttribute("locked");
      }
      if (name === "disabled") {
        this.#disabled = this.hasAttribute("disabled");
      }
      if (name === "value") {
        this.value = newValue ? Number(newValue) : null;
      }
      this.render();
    }
  }

  handleClick(event) {
    event.preventDefault();
    this.#locked = !this.#locked;
    this.render();
  }

  handleChange(event) {
    this.value = Number(event.target.value);
    this.dispatchEvent(
      new CustomEvent("change", {
        bubbles: true,
        composed: true,
      }),
    );
  }

  connectedCallback() {
    this.render();
  }
}

customElements.define("wc-easy-question", WCEasyQ);

Except for the hardcoded label values, this is an inherently reusable component. Still, the next element—the daily question elements—was far more custom, not least because it was my first attempt at using a component as a table row. This is the markup I needed to produce:

<tr is="wc-easy-question-row"
    name="Domain-1-1"
    value="2">
  <th class="weight-normal vertical-align-bottom">
    1. Some <span class="bold">strong</span> and important question?
  </th>
  <td class="center-align">
    <label class="radio" title="Not limied at all">
      <input type="radio"
             name="Domain-1-1"
             value="0"
             class="middle center">
      <span></span>
    </label>
  </td>
  <td class="center-align">
    <label class="radio" title="A little limited">
      <input type="radio"
             name="Domain-1-1"
             value="1"
             class="middle center">
      <span></span>
    </label>
  </td>
  <td class="center-align">
    <label class="radio" title="Moderately limited">
      <input type="radio"
             name="Domain-1-1"
             value="2"
             class="middle center"
             checked="">
      <span></span>
    </label>
  </td>
  <td class="center-align">
    <label class="radio" title="Very limited">
      <input type="radio"
             name="Domain-1-1"
             value="3"
             class="middle center">
      <span></span>
    </label>
  </td>
  <td class="center-align">
    <label class="radio" title="Totally limited / unable to do">
      <input type="radio"
             name="Domain-1-1"
             value="4"
             class="middle center">
      <span></span>
    </label>
  </td>
</tr>

As you can see, we’re extending the HTMLTableRowElement and making it form-associated. This is the whole implementation:

import { v4 as uuidv4 } from "https://cdn.skypack.dev/uuid";

class WCEasyQRow extends HTMLTableRowElement {
  #value = null;
  #disabled = false;

  static get observedAttributes() {
    return ["value", "name", "disabled"];
  }

  static formAssociated = true;

  constructor() {
    super();
    this.labels = [
      "Not limied at all",
      "A little limited",
      "Moderately limited",
      "Very limited",
      "Totally limited / unable to do",
    ];
    this.name = uuidv4();
  }

  render() {
    this.removeEventListener("change", this.handleChange);
    const tds = this.querySelectorAll("td");
    for (const td of tds) {
      td.remove();
    }
    this.insertAdjacentHTML("beforeend", this.html);
    this.addEventListener("change", this.handleChange);
  }

  get html() {
    return this.labels
      .map(
        (label, i) => `
          <td class="center-align">
            <label class="radio"
                   title="${label}">
              <input type="radio"
                     name="${this.name}"
                     value="${i}"
                     class="middle center"
                     ${this.#disabled ? "disabled" : ""}
                     ${this.#value === i ? "checked" : ""} />
              <span></span>
            </label>
          </td>`,
      )
      .join("");
  }

  set name(value) {
    this.setAttribute("name", this.name);
  }

  get name() {
    return this.hasAttribute("name") && this.getAttribute("name") !== null
      ? this.getAttribute("name")
      : this.name;
  }

  set disabled(value) {}

  get disabled() {
    this.#disabled = this.hasAttribute("disabled");
    return this.#disabled;
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      if (name === "value") {
        this.#value = newValue ? Number(newValue) : null;
        this.render();
      }
      if (name === "disabled") {
        this.#disabled = this.hasAttribute("disabled");
        this.render();
      }
    }
  }

  connectedCallback() {
    this.render();
  }

  set value(value) {
    this.#value = Number(value);
    this.setAttribute("value", this.#value);
  }

  get value() {
    return this.hasAttribute("value") && this.getAttribute("value") !== null
      ? Number(this.getAttribute("value"))
      : null;
  }

  handleChange(event) {
    this.value = Number(event.target.value);
    if (this.#value !== Number(event.target.value)) {
      this.dispatchEvent(new Event("change"));
    }
  }
}

customElements.define("wc-easy-question-row", WCEasyQRow, {
  extends: "tr",
});

Creating these components didn’t save me much time over copying and pasting the relevant markup and changing the text and names of the radio inputs. Still, it did mean that should I discover an issue when creating the inputs, I only had to address the problem in one file for all the relevant inputs to be fixed simultaneously. And more importantly, it meant the classes would act as templates for future implementations. I’m not adding them to npm as they are only helpful to me, but I can think about abstracting the classes in the future so that they might be more flexible. The second example – the extended table row element – is unsuitable for reuse in any current project I’m working on, but it might be in the future.

Interestingly, I did have a minor issue with them, perhaps due to the sheer number of moving parts. I originally had a render function that ran once when the component mounted. I then did all sorts of interesting internal DOM manipulation, but every so often, the elements would not reflect the changes, so every time something needs to change in the DOM, I re-render it, and things don’t mess up now.

My primary concern is the hard coding of the values, but I’ve read that passing complicated object data in an attribute is considered a bad thing, so I’m not sure how best to address that other than using slots. Sure, I could do complicated things like using JSON strings or Base64 encoded data, but that seems to be getting away from the spirit of web components. I’ve read about passing data using properties. Still, for this example, at least, the only hard-coded values are the labels for the inputs, and they stop the same, so I might as well leave them like that; making them properties might increase the utility of the classes and encourage internalisation and reuse.

Perhaps making them proper web components suitable for use by others and thus worthy of popping into npm might be a job when I have a little time. The inclusion of the specific CSS for the first component might also be an attribute, and this would increase that component's utility.

But, going back to Voltaire, as you’ll doubtless clock from this rambling, while I embraced the less-than-perfect (no utility outside the specific project and no aim to upload to npm) in creating these two components, creating them meant that I could see how they might be made more perfect; I particularly enjoyed the whole locking mechanism, and this is something that I’d like to explore more, though with an appreciation that this might not be required elsewhere – perhaps the locking functionality needs to be made optional before I abstract the class further. You’ll also notice the inclusion of uuidv4 so that, should I forget to add the name when adding the component, the radio buttons will all share the same name, preventing more than one radio button from being checked at a time.

In this instance, I chose Practicality over Purity, and that's fine in my books... for now.

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.

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`