Repository: typestack/class-validator
Pull request #1796 (2022-11-20 12:10:27)

feat: add `@IsTimeZone` validator
## Description
This PR adds a `@IsTimezone` validator that checks if the given value is a valid IANA timezone or not. 

## Checklist
<!-- Replace  the [ ] with [x] to check the boxes. -->
- [x] the pull request title describes what this PR does (not a vague title like `Update index.md`)
- [x] the pull request targets the *default* branch of the repository (`develop`)
- [x] the code follows the established code style of the repository
  - `npm run prettier:check` passes
  - `npm run lint:check` passes
- [x] tests are added for the changes I made (if any source code was modified)
- [x] documentation added or updated
- [x] I have run the project locally and verified that there are no errors

### Fixes
<!-- If there is no issue being resolved, open one before creating this pull request. -->
<!-- If the PR doesn't fully resolve the issue, replace 'fixes' with 'references'. -->
references https://github.com/typestack/class-validator/issues/1795



Patch:
diff --git a/README.md b/README.md
index 4d1cccc..c44e8b5 100644
--- a/README.md
+++ b/README.md
@@ -885,6 +885,7 @@ isBoolean(value);
 | `@MaxLength(max: number)`                       | Checks if the string's length is not more than given number. |
 | `@Matches(pattern: RegExp, modifiers?: string)` | Checks if string matches the pattern. Either matches('foo', /foo/i) or matches('foo', 'foo', 'i'). |
 | `@IsMilitaryTime()`                             | Checks if the string is a valid representation of military time in the format HH:MM. |
+| `@IsTimeZone()`                                 | Checks if the string represents a valid IANA time-zone. |
 | `@IsHash(algorithm: string)`                    | Checks if the string is a hash The following types are supported:`md4`, `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `ripemd128`, `ripemd160`, `tiger128`, `tiger160`, `tiger192`, `crc32`, `crc32b`. |
 | `@IsMimeType()`                                 | Checks if the string matches to a valid [MIME type](https://en.wikipedia.org/wiki/Media_type) format |
 | `@IsSemVer()`                                   | Checks if the string is a Semantic Versioning Specification (SemVer). |
diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts
index 41b843f..d8e9b56 100644
--- a/src/decorator/decorators.ts
+++ b/src/decorator/decorators.ts
@@ -111,6 +111,7 @@ export * from './string/IsPostalCode';
 export * from './string/IsRFC3339';
 export * from './string/IsRgbColor';
 export * from './string/IsSemVer';
+export * from './string/IsTimeZone';
 
 // -------------------------------------------------------------------------
 // Type checkers
diff --git a/src/decorator/string/IsTimeZone.ts b/src/decorator/string/IsTimeZone.ts
new file mode 100644
index 0000000..3504d0a
--- /dev/null
+++ b/src/decorator/string/IsTimeZone.ts
@@ -0,0 +1,43 @@
+import { ValidationOptions } from '../ValidationOptions';
+import { buildMessage, ValidateBy } from '../common/ValidateBy';
+
+export const IS_TIMEZONE = 'isTimeZone';
+
+/**
+ * Checks if the string represents a valid IANA timezone
+ * If the given value is not a valid IANA timezone, then it returns false.
+ */
+export function isTimeZone(value: unknown): boolean {
+  try {
+    if (typeof value !== 'string') {
+      return false;
+    }
+
+    /** Specifying an invalid time-zone will raise a `RangeError: Invalid time zone specified` error. */
+    Intl.DateTimeFormat(undefined, { timeZone: value });
+
+    return true;
+  } catch (exception) {
+    return false;
+  }
+}
+
+/**
+ * Checks if the string represents a valid IANA timezone
+ * If the given value is not a valid IANA timezone, then it returns false.
+ */
+export function IsTimeZone(validationOptions?: ValidationOptions): PropertyDecorator {
+  return ValidateBy(
+    {
+      name: IS_TIMEZONE,
+      validator: {
+        validate: (value, args): boolean => isTimeZone(value),
+        defaultMessage: buildMessage(
+          eachPrefix => eachPrefix + '$property must be a valid IANA time-zone',
+          validationOptions
+        ),
+      },
+    },
+    validationOptions
+  );
+}


Test patch:
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index 49bc660..dae3640 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -184,6 +184,7 @@ import {
   isPostalCode,
   IsSemVer,
   isSemVer,
+  IsTimeZone,
 } from '../../src/decorator/decorators';
 import { Validator } from '../../src/validation/Validator';
 import { ValidatorOptions } from '../../src/validation/ValidatorOptions';
@@ -3829,6 +3830,28 @@ describe('IsMilitaryTime', () => {
   });
 });
 
+describe('IsTimeZone', () => {
+  class MyClass {
+    @IsTimeZone()
+    someProperty: string;
+  }
+
+  it('should not fail for a valid IANA timezones', () => {
+    const validValues = ['Asia/Kathmandu', 'America/New_York', 'Europe/Paris', 'Europe/Berlin'];
+    return checkValidValues(new MyClass(), validValues);
+  });
+
+  it('should fail for invalid timezone format', () => {
+    const invalidValues = ['Asia/Pokhara', 'America', 'New_York', '/Paris'];
+    return checkInvalidValues(new MyClass(), invalidValues);
+  });
+
+  it('should fail for invalid values', () => {
+    const invalidValues = [undefined, null, 'Asia-Kathmandu'];
+    return checkInvalidValues(new MyClass(), invalidValues);
+  });
+});
+
 describe('isPhoneNumber', () => {
   describe('with region', () => {
     const validValues = [