Get the Difference Between Two Arrays in JavaScript

Sometimes we want to compare the difference between two arrays in JavaScript and return the remaining values in a new array.

This can be useful for inventory or purchase management where we could compare the selected items against the available stock, for example.

In this tutorial, we will cover two approaches to this:

  1. A single set of unique items in an array.
  2. A main array containing duplicate items.

Let’s get cracking!

Continue reading “Get the Difference Between Two Arrays in JavaScript”

Count the Occurrence of a Selection of a Cell in Google Sheets with Apps Script

A mate reached out to me last week asking if there was a way to monitor the times any article in a Google Sheet list goes through an editing stage before being published. From there, he wanted to report the number of edits on the article in an adjacent cell.

Take a look at the example:

As you can see in the example, whenever “NEEDS EDIT” is selected from the dropdown menu, then the counter column for the selected row item is updated automatically.

In this tutorial, we will cover how to create this for a single Google Sheet tab or for multiple selected tabs. I’ll also wrap up with a script to calculate just the aggregate count of changes to all the rows in the selected range and report it in a single cell.

Check out the video tutorial that covers the basics and grab the Starter Sheet to play along:

The Video

Scheduled release 6 Dec 2022 9am GMT+10

Generally better to view it from YouTube.

Starter Sheet

Here is a link to the starter sheet copy the sheet and head to Extensions > Apps Script in the menu.

Starter Sheet

The onEdit(e) Simple Trigger

Everything starts with the onEdit(e) simple trigger. This is a special trigger in Google Apps Script called a Simple Trigger that will run every time you edit your Google Sheet. If Apps Script sees the onEdit() function in your project it runs automatically.

Every time onEdit() is executed, it will retrieve an event parameter that is commonly referenced as "e" (onEdit(e)). The event object can return things like:

  • The current value
  • The previous value
  • The edited range
  • The source spreadsheet
  • The trigger id
  • The authorisation mode

We will be using a few of these in our code below.

Unless you have a very short coding task, it is generally good practice to reference another function that will be executed when onEdit() is run rather than coding directing within the onEdit() function.

Take a look at the three examples I added for my test project for this tutorial:

You can see that each one of these functions will be run one after the other when a cell is edited and calls onEdit().

Count Every Time The Cell Changes

In this example, we add a count to our adjacent counter cell each time the cell is edited.

Note that we take the event (e) as our parameter for our countEveryTimeCellIsChanged(e) function.

Set the Variables

First, let’s assign all our variables at the top of our function to make it easier for us to change should we need to down the track.

We have 3 variables that we need to include here. They won’t change so we will make them a constant variable:

  1. tgtSheet – This is the target sheet tab name. For our example, this is assigned to “Single List”.
  2. tgtCol – This is the target column or the column that has the values that will need to be monitored for changes. In our example, this is the status column, column 3.
  3. counterCol – This is the column where we display our change count. For us, this is column 4, but it could be any selected column in the sheet.
Numbers correspond to the points above.

You can stop here if you want to copy and paste the script into your own project and update the variables for your own purposes. Or continue to learn how it all works.

Create and Publish a Google Workspace Add-on with Apps Script Course

Need help with Google Workspace development?

Go something to solve bigger than Chat GPT?

I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.

Schedule a free consultation today to discuss your needs and get started or learn more about our services here.

Get the Correct Sheet Tab and Column

Lines 33-38

We don’t want to end up counting the wrong column or updating a completely different sheet tab by mistake. This means we need a way to check if we are in the right spot.

The first thing we need to do is check the range we are currently editing. This can be done by grabbing the range object in our event parameter. Line 33

We now have access to the Google Apps Script range class. From this class, we can get the sheet class and also the range column numberLine 34-35

Using the JavaScript ‘if’ statement we first check that the sheets name (sheet.getName()) is equal to our assigned target sheet name and if it is, then check that the selected column is equal to the target column. Line 38

Update the Counter Cell

Lines 40 – 48

We need to update the adjacent counter cell in the same row that was changed when edited.

We can use the range class again with the getRow() method. This will give us a row number. Line 40

const row = range.getRow();

Next, using the sheet variable we created earlier we can call the getRange() method to get the counter cell. For a single cell, this method can take two variables the row and the column number. Here, we add in these two variables we collected earlier.

const counterCellRange = sheet.getRange(row, counterCol);

Now we need to add one to the current count in our counter cell. This means that we first need to collect the current value in the cell, which we do with the getValue() method.

We need a way to check if the value in the counter cell is empty. If it is then we want to give it a value of 1 otherwise we want to add 1 to the current value.

const counterCellVal = counterCellRange.getValue();

This can be achieved with a ternary operation. Think of a JavaScript ternary operator as a single line if statement in a way.

The ternary operator below states that if the value is less than zero (empty) then add one otherwise add one to the counter cell value.

const updateCounterCell = (counterCellVal < 0)? 1 : counterCellVal + 1;

Finally, we update the counter cell:

counterCellRange.setValue(updateCounterCell);

Done!

 

The following scripts will modify this code. We will touch on the changes only in the code discussion of each script.

Count Every Time The Cell Changes To the Selected Value

In this scenario, we only want to count when a cell changes to a specific value. For our example, this value is  “NEEDS EDIT”.

First off, we need to store a target value that we want to monitor for and if that value appears add one to our counter cell. Here on line 29, we set our target value to “NEEDS EDIT”. 

const tgtVal = "NEEDS EDIT"

You can change this to the value that you need for your project.

Next, we need to check if the cell that was edited was edited to the new target value before we update the counter cell.

To do this we first get the value of the selected range (Line 42):

const cellVal = range.getValue();

Then we create an if statement checking that the cell value matches the target value on line 43 and closes on line 54.
if(cellVal === tgtVal){

Apply The Script to Multiple Selected Google Sheets

What if we have different editorial teams or genres handled by different editors? We might want to put the data for each team in a different sheet tab to make it easy for monitoring. Here we will need to check all the selected sheets for any changes.

In our example now we want the “Team 1” and “Team 2”  sheet tabs. Both tabs are identical in format.

Monitoring multiple sheet tabs for changes to a cell value onEdit in Google Sheets with Apps Script

Let’s see how our code works now:

Two changes need to be made to the script here.

On line 25, we need to change the tgtSheet variable to tgtSheets and include an array of all the Google Sheet tabs we need to monitor with our Apps Script code.

const tgtSheets = ["Team 1", "Team 2"]; // The sheet names the data is on.

Then on line 38, we need to replace the first part of the if statement with a way that will look through our array of sheets and check if the current sheet contains the selected name.

This can be achieved with the JavaScript includes function. This function appends a selected array (In our case, the target sheets array) and takes a target value as an argument. Our target value is the select sheet that this being edited.

... tgtSheets.includes(sheet.getName()) ...

Get the Aggregate Count of the Changes

This final script checks all the cells in the target column in a single Google Sheet tab and checks if they have been changed to “NEEDS EDIT”. Then records the aggregate count of all the cells in a single counter cell in a separate sheet tab.

In our example, we will monitor the “Single  List” sheet tab for our desired change and record the total count of changes for all rows in the “Aggregate” sheet tab.

Calculate the aggregate of all changes to cells to the target value in Google Sheets With Apps Script

Check out the code:

First, we have replaced the counterCol variable with the counterCell variable and set that to cell A2 of the ‘Aggregate’ sheet tab.

const counterCell = 'Aggregate!A2'; // The column to update the counter.

Next, we don’t need to get the row number this time so we have deleted this variable.

Lastly, on line 44, we use the source property of the event (e) object to grab the current spreadsheet. This allows us to use the getRange() method to select our counter cell.

Conclusion

There should be enough samples there for you to work out your own combinations. If you want to test your new skills out, try to:

  1. Create an aggregate count of multiple sheet tabs.
  2. Move the counter column to another location in the sheet.
  3. Check for different target values in different sheets with different locations.

If you get stuck check out some of the other popular tutorials on using onEdit():

It’s always nice to hear how people use these scripts in their own projects and it can be inspiring to share your use cases with others. Feel free to share your experience and examples in the comments below.

Finally a big thanks to Mike Kanert for the inspiration for this post. Editor, writer, artist, actor, educator, polymath and all-around nice fella, you can find Mike on Instagram @Unremarkable_Us for his latest comic series made by parents for parents:

Unremarkable Us

If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.

~Yagi

How to Validate Specific Users on a Web App in Google Apps Scripts

You’ve created an awesome Google Apps Script web app for your secret society within your Google Workspace organisation or …dom! dom! DOM! … the world. The problem is that you only want to share your web app with the worthy. Those selected few. 🐐🛐🛐🛐

How do you do this? How to prevent this most coveted of apps from reaching the wrong hands?

It’s actually surprisingly simple.

In this tutorial, we will explore how to validate selected users to provide access to your web app. For our example, we validate users based on whether or not they have edit access to a Google Drive file ( a common occurrence). In the discussion, we will also look at alternative ways of validating emails.

One of the bonuses of the approach we will go through is that it can also be easily adapted for use in Google Workspace Add-ons, and Editor Add-ons like sidebars and dialogue boxes.

We’ll start off with an example and then move to a quick-use guide for those of you who just want to get in and apply the code to your own project. Then for those who want to know how it all works, I’ll dive into the details.

Let’s get started!

Continue reading “How to Validate Specific Users on a Web App in Google Apps Scripts”

Create a custom automated Table of Contents for your Google Sheets tabs with Google Apps Script (Updated Feb 2022)

In this tutorial, we will cover creating a custom Table of Contents that lists your Google Sheets tabs on its own tab using Google Apps Script. This Table of Contents (TOC) will update whenever you open your sheet or choose to update it with a button.

But Yagi! Can’t I just check the list of tabs from the All Sheets button in the bottom right or scroll across bottom list of tabs until I find what  I need?

Sure you can, but sometimes the sheet tab name just doesn’t properly explain what is in your sheet. There is a word limit to the tags and that bottom tab bar will get awfully cluttered if you start creating verbose tags. 🐐

On most of your sheets, you will probably have a title or description perhaps on the first row. This will probably more accurately detail what is occurring. You might also have some universal details that you have on all your sheets that you want to display on your table of contents tab.

Finally, you might only want certain tabs to be in your Table of Contents.

Note! As always, read as much as you need or settle in to read the whole thing. 

Features

Our code contains the following features:

  • Generate a table of contents on a separate sheet tab. Any time we create a new sheet tab it will be added to our table of contents either on the next load of the Google Sheet or manually when editors of the sheet click a button.
  • Sort the sheet tabs alphabetically. So that your users have an easily indexable list. The can be removed.
  • Dedicated ‘Notes’ Sheet Tab for you to easily edit to change how you want your Table of Contents to be displayed. Make changes to how you want your Table of Contents to look right inside your Google Sheet.
  • Choose the location cell of your tab titles. Assign what cell your titles are going to be in.
  • Identify what Sheet Tabs you don’t want to be included in your Table of Contents. You might not want to share all of your tabs, right? For example, it seems a little silly to share your Table of Contents tab.
  • Optional addition of your Sheet Tab name included in the TOC. 

Let’s take a look at what we will be making:

If you are following along with the code, here is the raw Google Sheet.

Table of Contents – Follow Along – Empty Code

There will be a bunch of example Sheet Tabs already there for you. Just go to File > Make a copy. Then open the Google Apps Script Editor (Extensions > App Script).

The Code

Code.gs

Quick Use Guide

Using the Template

The fastest way to get started is to grab a copy of the template file below (File > Make a copy).

Table of Contents – Template

Enter in all of your extra Sheet Tabs. Or as many as you have. You can always add more and your sheet will update your Table of Contents (TOC) next time the sheet opens.

Enter all of your parameters for your TOC (more on this in a bit) in the Notes sheet tab and click the button to run the code for the first time and go through the process of accepting permissions to run the code if you are happy with it.

Running Google Apps Script for the First time. What’s with all the Warnings!

Yeap, when you copied the sheet across a copy of the code was transferred across with it. Cool, hey!?

Then go to your assigned Contents Sheet Tab and format it how you like. Don’t worry it won’t be removed the next time the TOC  is updated.

Hide and protect your notes tab and any anything else you want hidden and protected and you are all done.

Adding Your Table of Contents toolkit to an existing Sheet.

First, create a new Google Sheets tab and label it as Content or whatever you want to name your TOC. Format it how you like.

Then go to the Table of Contents – Template and either:

  1. Make a copy of the Notes tab data. Create a Notes tab and paste it into the exact same location.
  2. Right-click on the Notes tab of the Template Google Sheet. Select Copy to > Existing spreadsheet. Then search for the current Google Sheet you are working in.
copy google sheets tab to existing spreadsheet
Click to expand!

Then copy the Google Apps Script code above and paste it into your code editor.

What if I want to put the Notes setup in another place?

If you want to put the setup data in another Google Sheets tap, you will need to update the NOTES_SHEET variable on line 2 of the Code.gs file.

If you want to move the setup data to start at a different cell you will need to scroll down to the getVariables() function and update the following line:

Ensure that the range is 30 rows deep and 2 rows wide and you will be good.

Completing the Setup Data in the Notes Sheet Tab

Google Sheets Table of Contents setup page

All grey areas indicate the places you need to fill out. There are instructions for each part. If you need an example, hove over the input fields and a note will popup with an example.

1. Select the location of your Title

All of your sheets will probably have the exact same location of their Title. Here you will provide the cell. If the title is merged over multiple cells, select the first cell in the top-left.

An example of a valid input would be, A2 or B4.

2. Do you want to add the sheet tab name to your Table of Contents?

You can essentially choose to display your table of contents with a counter and the title:

google sheet Table of contents counter and title

Or include the Sheet Tab name as a third row.

google sheet Table of contents counter title sheet tab name

Having the sheet tab name can be really handy if you want to create other columns of data for your Table of Contents using the INDIRECT Google Sheets function. Take a look at this example:

additional table of contents items with INDIRECT in Google Sheets
Click to Expand!

Here is the formula, have a try yourself if you are playing along:

=IF(C3="","",INDIRECT(C3&"!A2"))

Check out this example sheet where we have added the name and students who have grades remaining to the TOC.

Table of Contents with Extra Columns using INDIRECT

 

3. When a TOC link is clicked where should we navigate to?

You can choose what cell you want your uses to be navigated to when they click the link in the TOC.

You might not always want your users to go straight to cell A1. Perhaps you want to get them to work straight away and navigate them to the first cell of the data they need to enter say, cell B6 for example.

4. Name the Sheet Tab Where you are storing your TOC.

This will automatically be set to Contents, but you might want to call it TOC or list, or something.

Note that this will automatically update cell A20 so that it is excluded from the contents. If you are feeling a little eccentric then you can delete this.

5. The start row of the TOC

Choose the row that your Table of Contents, including the headers, will go. You might want to give your contents sheet tab a title so you may wish to indicate row 2 here.

6. Excluding sheets

You can list all the sheet tabs you want to be excluded here. the TOC sheet and the Notes tab is in by default but you can add up to 12 sheets you want to be excluded.

This might be useful for hidden sheets or sheet that don’t follow the Title pattern.

7. Run the code

To generate the TOC for the first time, run the code and got through the permission process. you will only have to do this once.

If you add more sheet tabs you can either click the button again or reload the page.

That’s all there is to set up your own Table of Contents for your Google Sheet. If you want to dive into the code with me, head down to the next chapter. If you are happy with this free tool, hit the like button and subscribe. Finally, donations help keep this site alive and reduce the ads I need to put on here. If you want to donate and support me there is a button up in the top-right of the sidebar.

Create and Publish a Google Workspace Add-on with Apps Script Course

Need help with Google Workspace development?

Go something to solve bigger than Chat GPT?

I can help you with all of your Google Workspace development needs, from custom app development to integrations and security. I have a proven track record of success in helping businesses of all sizes get the most out of Google Workspace.

Schedule a free consultation today to discuss your needs and get started or learn more about our services here.


Code Breakdown

Global Variables

Not much going on in the Global Variables. If you have your Table of Contents (TOC) setting in another sheet then you will need to update this.

onOpen()

The onOpen() function is a reserved custom trigger in Google Apps Script.  It can take one argument commonly notated as e for the event. For us, we do not need the event argument so we have left it out.

As its name suggests the onOpen() trigger runs when the document is first loaded. The function’s only task is to run the updateTOC() trigger.

It is generally a good practice to not bloat these custom trigger. Instead, use them to refer to functions that complete specific tasks.

updateTOC()

This is the main driving function. It will review the setup details for the TOC and then collect all the Google Sheet tab excluding the ones indicated. Then it will add the table of contents to the assigned sheet and then sort it.

Acquiring the main variables

The first task is to grab the current active spreadsheet from the SpreadsheetApp class(Line 2)

We will need the unique ID of the spreadsheet to create our URL to link to other parts of the Google Sheet. Fortunately, we can do that easily with the getId() method.  (Line 3)

Finally, we need to collect all the values that have been submitted in the TOC settings block found in the Notes. This is done with the getVariables(SS) function. This function takes the current spreadsheet object as an argument and returns an object containing something like the following example: (Line 4)

Loading the sheet on the Table of contents tab

You’ll probably want your Google Sheet to open onto your Table of Contents each time. You can do this with the setActiveSheet() method that takes the sheet identifier.

Inside the brackets, you can see that we are using the getSheetByName() method to grab our selected sheet by calling on the TOC_vars object’s tocName key. In our example, we are referencing the Contents sheet tab.

If you don’t want the spreadsheet to open on your TOC you can comment this out or change the name of the sheet to your desired sheet tab name.

Set up the container variable that will store the TOC

In our TOC setting, we give you the option to include the Sheet Tab Name as well as the title and reference number.

We use a Javascript ternary operator to first check if the tick box has been selected. If it has, we add the reference number, title and sheet name headers and store it in our TOC_list variable. If it hasn’t we only store the reference number and title headers. (Line 3)

To create our reference number, we will add a count variable and set it to zero. (Line 5)

Looping through all the Google Sheets

Our first task is to iterate through all the sheet tabs. We can get a list of sheets using the getSheets() method. From there, we can apply the forEach JavaScript method to iterate through each sheet. (Line 3)

The forEach() method runs a function for each element in the array. We set sheet as our iterator variable.

The first task is to grab the sheet name from each sheet and store it in the sheetName variable. (Line 5)

As we look at each sheet name, we need to check it against our list of sheet tabs we want to exclude from our TOC. This is done on line 7 with an if statement that says that if the current sheet name is not included, or present, in our list of excluded sheet tabs, then continue with adding it to our table of contents.

We use the very fancy includes JavaScript method here to check if our current sheet exists in the list of excluded tab. Note the ! at the start which can be described as ‘not’ but more formally it means that we are looking for a false report on our if statement.

Next, we grab the title by using the getRange() Google Apps Script method to find the cell with the title in the currently iterated sheet. The location of the title is drawn from the TOC_vars.cellLoc value. The getRange() method can take, among other arguments A1notation to find a range. In our example, this is cell A1.

Lastly, we grab the sheet id. We will use this in a moment to create our sheet tab link.

Creating the link URL to each sheet tab

We’ll be making use of the Google Sheet HYPERLINK function to create a link for the title for each sheet. This function takes two arguments. The URL and the label for the URL. (Line 4)

Above this line, we will build the URL. There are three key points that we make modifications to the URL that you can see in the curly braces (${}).

  1. The SS_IDis the unique spreadsheet ID for the current document.
  2. The sheetID is the unique ID number for the sheet tab.
  3. The TOC_vars.navTo is the cell where we want to direct the user to in the sheet.

Adding the count, title/link (and sheet name)

After we first increase our count by one (Line 2) we then need to add the count, the title connected to our link and if we chose to add the sheet name, well… we add the sheet name. 🙄

Line 5s if statement checks if the user selected the sheet tab name. If they did we push the count, hyperlink and sheet name to the TOC_list. Otherwise, we just push the count and the hyperlink. (Lines 5-9)

This concludes the loop through the sheet.

Adding the Table of contents to the desired sheet

Our first task is to get the Table of Contents sheet object and store it in TOC_Sheet. (Line 3)

We will soon be pasting in our table of contents, but first, we will need to determine how deep our data is in rows and how wide it is. (Lines 6 & 7)

Just in case you delete out some Sheet tabs we want to make sure that you have a clean page, so we initially clear out the content. First, we grab the range with getRange() this time using 4 number parameters: (Line 8)

  1. Row Start
  2. Column Start
  3. Row height
  4. Col width

We have made the row height 100. It would be rare that you had more than 100 sheet tabs worth of rows in your TOC but you can always update this. Google is vague about the limit of sheet tabs.

Then we append the clearContent() method that clears the data from the range but not the formatting.

Finally, grab the range of the Table of Contents sheet again this time using our row height gathered from the length of the array. We then use setValues() to input our array of TOC into our sheet.

Sorting the data

Our last task is to sort our table of contents. This is an optional step and you can comment out these two lines if you don’t want to use it.

We want to make sure that our data is loaded into our Google Sheet before we sort it or we might have an error or the sort might be skipped entirely. This is called accounting for Race Conditions. This is done by applying the flush() method straight from the SpreadsheetApp class. (Line 2)

Next, we want to grab the row below our newly added header and then all the listed sheet tabs. We add the Google Apps Script sort() method to this which for us takes a single argument, sort ascending by the 2nd across. (Line 3)

getVariables()

The getVariables() function takes the spreadsheet as an argument and returns an object, for example:

The functions first task is to grab the range of Table of Contents settings data. First, it grabs the sheet by its name (Line 8).

Then it grabs the range. You can change this range value if you put the settings range in a different spot. Just make sure it is 2 columns wide and 30 rows deep. (Line 9)

Next, we grab the values of the settings range with the getValues method. (Line 10)

We then create the dataReference object and assign our setting values to our sheet. Each location is in a 2d array and we draw them out of our vals array by first referencing the row and then the column: (Lines 13-23)

vals[row][column]

To get our list of excluded sheet tabs we run an Immediately Invoked Function Expression (IIFE)(Line 19). First, we slice our vals array from row 19 onwards (Line 20). We then use the map method to iterate through the remaining rows selecting only the first column (Line 21). Finally, we filter out all the empty rows ( Line 22)

The dataReference object is then returned to updateTOC() function. Line 25

Conclusion

Creating a table of contents in a tab of your Google Sheet is pretty useful for your users to be able to quickly navigate to what sheet tab they need. I hope that after reviewing the code you can make some changes for your own project.

If you have been playing along, you might have noticed that there is no data validation to ensure the received TOC settings are correct. I kinda thought adding this extra level of complexity would detract from what  I was trying to achieve in the tutorial portion of this post.

However, running some validation either Google Sheets-side with Data Validation or inside your Google Apps Script will help reduce errors, but to be honest, not many folks are going to have access to the settings and those that do will probably figure out the error.

I was compelled to write this post based on interest in my Table of Contents from my previous post on using Google Sheets as a recipe folder. Check it out:

https://yagisanatode.com/2020/12/11/use-google-sheets-to-store-your-recipes-to-automatically-change-batch-sizes-and-recipe-amount-by-weight/

I would love to hear how you applied this Table of Contents creator in your own project. Feel free to comment below.

If you like this tutorial, give it a like so I know to keep em coming. If you want a regular dose you can subscribe down below. And if you want to support me, feel free to donate (top right-sidebar).

~Yagi

Google Apps Script: How to Add and Remove Editors to a Google Sheet based on Sheet Data (Updated Feb 2022)

Google Apps Script, Google Sheets, Javascript

Have you ever wanted to have a Google Sheet available for only those users who need to edit and then once they are done, take their edit permissions away to maintain the integrity of the sheet?

I know that I have come across these conditions a number of times in the past. Maybe you have a task list that you want to send your team each time a task is allocated to them. Once they let you know that they are done, by say, entering a set of values or checking a task complete box on the row they need to work on, you want to be able to remove their edit permissions from your sheet.

In this post, we have created a Google Apps Script that will add and remove editors to a Google Sheet based on the spreadsheet’s data. More specifically, this script will:

  • Grab the users name and email in each row along with whether or not they have complete the task or if the Google Sheet has been shared and sent to the user.
  • Share the assigned user to the Google Sheet.
  • Send an email to the user. A separate Google Sheet tab is added to the sheet so an administrator can add their custom email message.
  • Automatically check a reference column of checkboxes indicating that the assigned user has been shared as an editor on the Google Sheet and an email has been sent to them.
  • Once the task has been complete the user check the “Edit Complete” checkbox in their assigned row.
  • Either automatically each day or when the Google Sheets administrator clicks the button,  each user who has completed all assigned tasks is removed from having edit permissions to the Google Sheet.

The best way to probably understand this script is through an example…

Continue reading “Google Apps Script: How to Add and Remove Editors to a Google Sheet based on Sheet Data (Updated Feb 2022)”