Count Working Days in JavaScript With UK Bank Holidays
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
A simple, reliable regex for email validation in JavaScript is /^[^\s@]+@[^\s@]+\.[^\s@]+$/. It checks that there's some text, one @, a domain, a dot and an ending, with no spaces, and you use it with EMAIL_RE.test(email.trim()). It deliberately doesn't try to be perfect, because no regex can tell you whether an address really exists; only sending an email can do that. Below I compare it with two other popular patterns on ten real-looking addresses, show a "strict" regex that can freeze the page, and wire it into a form.
I've used this check on the contact form of my portfolio for a while. Before that I copied a long "RFC compliant" pattern from a forum, and it rejected a friend's perfectly valid Irish address. Every example below was run with Node 20 or in headless Chrome, and the outputs are copied exactly.
// email.js: a practical email check for forms
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function isValidEmail(value) {
const email = String(value).trim();
return email.length <= 254 && EMAIL_RE.test(email);
}
module.exports = { EMAIL_RE, isValidEmail };
Reading the pattern from left to right:
^ and $ anchor it to the start and end, so the whole string has to match, not just part of it.[^\s@]+ means "one or more characters that are not whitespace and not @". That's the part before the @.@ is a literal at sign, exactly one.[^\s@]+\.[^\s@]+ is the domain: some text, a literal dot (escaped as \.), and more text.The function also trims spaces, because people often paste an address with a space at the end, and limits the length to 254 characters, the practical maximum for an address.
Here are three patterns you'll come across: the simple one above, a popular "strict" one that only allows certain characters, and the pattern the HTML specification uses for <input type="email">, which is what browsers check against:
const { isValidEmail } = require("./email");
// The pattern browsers use for <input type="email"> (from the HTML spec)
const HTML_SPEC_RE = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
// A popular "strict" pattern you'll find in lots of tutorials
const STRICT_RE = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const samples = [
"aisha@example.co.uk",
"tom.riley+news@gmail.com",
" zoe@example.com ",
"o'connor@example.ie",
"rahim@localhost",
"no-at-sign.example.com",
"two@@example.com",
"spaces in@example.com",
"dot@example.",
"zoë@exämple.com",
];
const yes = (b) => (b ? "yes" : "no");
console.log("email simple strict html-spec");
for (const s of samples) {
console.log(
JSON.stringify(s).padEnd(29),
yes(isValidEmail(s)).padEnd(7),
yes(STRICT_RE.test(s.trim())).padEnd(7),
yes(HTML_SPEC_RE.test(s.trim())),
);
}
// Output:
// email simple strict html-spec
// "aisha@example.co.uk" yes yes yes
// "tom.riley+news@gmail.com" yes yes yes
// " zoe@example.com " yes yes yes
// "o'connor@example.ie" yes no yes
// "rahim@localhost" no no yes
// "no-at-sign.example.com" no no no
// "two@@example.com" no no no
// "spaces in@example.com" no no no
// "dot@example." no no no
// "zoë@exämple.com" yes no no
Things I learnt from this table:
o'connor@example.ie is valid; apostrophes are allowed in email addresses, and people with names like O'Connor or O'Brien get blocked by sites all the time.zoë@exämple.com. Internationalised addresses are rare, but they're real, and the simple pattern lets them through.rahim@localhost, because a domain without a dot is technically valid on a private network. On a public sign-up form you almost always want a dot, which is why I still add a JavaScript check on top of type="email".@, two @s, a space, and a missing ending.The full rules for email addresses (RFC 5322) allow quoted strings, comments and IP addresses in square brackets. Regexes that try to cover all of that are thousands of characters long, impossible to read, and still wrong in some cases. Worse, badly written patterns can be slow. Here's a short-looking pattern from an old forum answer, timed against my simple one on inputs that almost match:
const { isValidEmail } = require("./email");
// A pattern copied from an old forum answer: a + inside a group that also has +
const RISKY_RE = /^([a-zA-Z0-9]+\.?)+@example\.com$/;
for (const n of [20, 24, 28]) {
const input = "a".repeat(n) + "@example.co"; // almost matches, then fails
let t = performance.now();
RISKY_RE.test(input);
const risky = performance.now() - t;
t = performance.now();
isValidEmail(input);
const simple = performance.now() - t;
console.log(`${n + 11} chars: risky regex ${Math.round(risky)} ms, simple regex ${simple.toFixed(2)} ms`);
}
// Output:
// 31 chars: risky regex 39 ms, simple regex 0.20 ms
// 35 chars: risky regex 94 ms, simple regex 0.18 ms
// 39 chars: risky regex 1464 ms, simple regex 0.02 ms
Every four extra characters made the risky regex several times slower, and at 39 characters it blocked the JavaScript thread for well over a second. That's called catastrophic backtracking: the nested + inside a group that also has + gives the engine an exponential number of ways to try splitting the text. In a browser the page freezes; on a Node server, one request can stall everyone else's. Your timings will differ, but the pattern of growth won't. The simple regex has no nested quantifiers, so it stays fast whatever you throw at it.
In a real form I combine three things: type="email" so mobile keyboards show the @ key, the regex check on submit, and a clear message next to the field:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Email check</title>
</head>
<body>
<form id="signup" novalidate>
<label for="email">Email</label>
<input id="email" name="email" type="email" required autocomplete="email">
<p id="email-error" aria-live="polite"></p>
<button type="submit">Sign up</button>
</form>
<script>
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const form = document.getElementById("signup");
const input = document.getElementById("email");
const error = document.getElementById("email-error");
form.addEventListener("submit", (event) => {
const email = input.value.trim();
let message = "";
if (email === "") message = "Please enter your email address.";
else if (!EMAIL_RE.test(email)) message = "That doesn't look like an email address. Check for a missing @ or dot.";
error.textContent = message;
input.setAttribute("aria-invalid", String(message !== ""));
if (message) {
event.preventDefault();
input.focus();
}
});
</script>
</body>
</html>
I tested it in headless Chrome by submitting an empty field, an address without a dot, and a valid address:
"" -> submitted=false, error="Please enter your email address."
"aisha@example" -> submitted=false, error="That doesn't look like an email address. Check for a missing @ or dot."
"aisha@example.co.uk" -> submitted=true, error=""
The novalidate attribute turns off the browser's built-in pop-up so my own message is shown instead, which looks the same in every browser. aria-live="polite" makes screen readers read out the error, and aria-invalid marks the field. My post on HTML form input types and validation covers type="email", required and the other built-in checks in more detail, and MDN's page on <input type="email"> lists every attribute it supports.
JavaScript validation is there to help honest users fix typos quickly. It doesn't protect anything, because anyone can turn JavaScript off or send a request straight to your server. Check the email again on the server before you store it or send to it. In PHP that's filter_var($email, FILTER_VALIDATE_EMAIL), which I show in my PHP form validation and sanitisation guide. If you're sending the form with JavaScript, my fetch POST JSON example shows how to display the server's error message back to the user.
A regex can only check the shape of an address. aisha@example.co.uk passes every pattern above, but nobody reads that inbox. If it matters that the address is real, for example for password resets, send a confirmation link and only activate the account once it's clicked. That's the only check that is 100% accurate.
^ and $. Without them, "hello a@b.co world" passes because part of it matches.. means "any character", so a@bxco would pass.g flag with test(). A global regex remembers lastIndex between calls, so the same valid address can fail every second time.@ can be. Most providers ignore case, so lower-casing for comparison is fine; just store what the user typed.For most forms, /^[^\s@]+@[^\s@]+\.[^\s@]+$/. It catches real typos, accepts unusual but valid addresses, and can't cause catastrophic backtracking. Confirm the address by email if it really matters.
Use an <input type="email"> and call input.checkValidity(), which uses the browser's built-in pattern. Bear in mind that it accepts addresses without a dot in the domain, such as name@localhost.
It's a good first layer and gives mobile users the right keyboard, but it allows dotless domains and can be bypassed. Add your own check if you need a dot, and always validate again on the server.
No. A regex only checks the format. To know an address exists and belongs to the user, send it a confirmation link or code.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
Work out a First, 2:1 or 2:2 from module marks: credit-weighted averages, 30:70 or 1:2 year weightings, rounding at 69.5 and the mark you need.
How UK student loan repayments are worked out in 2026/27: per-period thresholds, 9% over, rounding down, two plans at once and a floating-point trap.