DataMasque Portal

Date Parsing in DataMasque

DataMasque has two ways to parse dates. They can be parsed with date format preservation, or by specifying a date format.

Date Format Preservation

Date format preservation allows preserving the format of unstructured dates while masking the actual date underneath.

For example, if one document contained the same date written four different ways: 14/03/1978, 14-Mar-1978, March 14, 1978, and 19780314, DataMasque is able to understand these are the same date with different formats, and mask them, while preserving their respective formats.

DataMasque also handles a shortened part (Jul), a missing part (no day or year), and weekdays, and keeps whatever mix of lower, upper, or title case the words use.

Note that if date format preservation is enabled, a typecast following a date mask is not required.

Note: Currently only the from_random_date mask supports date format preservation.

Examples

For example, suppose from_random_date internally generates a replacement date: 2014-08-15, which was a Friday. The table below shows what happens when this generated date is used for masking various formats with date_format_preservation enabled.

Input Output
Fourth of July Fifteenth of August
december ninth 2019 august fifteenth 2014
Saturday, 16 March 1988 Friday, 15 August 2014
24 October, 9 pm 15 August, 9 pm
January the 1st August the 15th
1st day of January, 2023 15th day of August, 2014
mid-nov mid-aug
THURSDAY the 5th of April FRIDAY the 15th of August
thurs fri
October1962 August2014
Jul, 1955 Aug, 2014
3rd of Sept '99 15th of Aug '14
13-Mar-2005 15-Aug-2014
1/22/2012 at 5pm 8/15/2014 at 5pm
2001-03-15 14:32:17 2014-08-15 14:32:17
19730316 20140815

Note that the examples given above are not exhaustive and a lot more formats are supported.

date_format_preservation is enabled by default for compatible date submasks under the unstructured_text mask and disabled everywhere else. This means you most likely do NOT need to enable date_format_preservation unless you have an unstructured text column as described in Example: Unstructured Text.

Example: Unstructured Text

Unstructured text can contain structured (2005-08-19) or unstructured (19th of Aug, 2005) dates. For the clinical notes example below, an ai_detect matcher is used to detect DATE entities which are then masked with date_format_preservation. Under an unstructured_text mask, date format preservation is enabled by default, so the ruleset below does not contain an explicit date_format_preservation block with enabled: true.

Ruleset

version: "1.0"
tasks:
  - type: mask_table
    table: medical_records
    key: id
    rules:
      - column: clinical_notes
        masks:
          - type: unstructured_text
            matchers:
              ai_detect:
                - label: DATE
            masks:
              - label: DATE
                masks:
                  - type: from_random_date
                    min: "1980-01-01"
                    max: "2000-01-01"

Example output:

Before After
Admitted on the 3rd of April 2024 for review. Admitted on the 21st of September 1991 for review.
Patient DOB: 2008/11/15 Patient DOB: 1994/03/22

Parameters

All available settings are shown in the block below. All the shown settings are the defaults.

date_format_preservation:
  enabled: false
  date_order: month_first
  on_unknown_text:
    action: remove
    exempt: []
  fallback_date_format: '%Y-%m-%d'

date_format_preservation blocks can be added at the column level or at the label level below an unstructured_text mask.

Warning: date_format_preservation reads dates out of text, so use it on text columns only. With date_format_preservation enabled, from_random_date fails the run when it receives a date or datetime from a DATE or TIMESTAMP column or from a previous mask.

  • enabled (optional): Turn date format preservation on or off. Defaults to true for an unstructured_text mask label and false everywhere else. See How Settings Cascade.
  • date_order (optional): Which number is the day when a date reads two ways, such as 03/04/25. One of day_first or month_first. Defaults to month_first.
  • on_unknown_text (optional): What to do with a word beside the date that the parser does not know. action can be one of retain, remove, or treat_as_invalid, defaults to remove. exempt defaults to []. See On Unknown Text.
  • fallback_date_format (optional): The format to use for a null value or if parsing has failed. Accepts Python date format codes. Defaults to %Y-%m-%d.

Date Order

For ambiguous dates such as 03/04/2025, setting date_order: day_first makes the parser interpret 03 as the day and 04 as the month. For dates such as 15/05/2017 and January the 1st, 1998, there is no ambiguity so date_order has no effect.

Examples

Given the following:

  • a column called unstructured_dates, with ambiguous dates that must be parsed using the day_first rule
  • a column called constant_column, which contains the same value for every row

Setting constant_column in hash_columns makes from_random_date generate the same date for every row. Suppose this generated date is 1992-10-18, i.e. the 18th of October 1992.

- column: unstructured_dates
  hash_columns:
    - constant_column
  date_format_preservation:
    enabled: true
    date_order: day_first
  masks:
    - type: from_random_date
      min: "1980-01-01"
      max: "2000-01-01"

The example masked values then become:

Input Output
05-08-2021 18-10-1992
15/08/2021 18/10/1992
08.15.21 10.18.92
1st of Jan 18th of Oct
mar 2017 oct 1992

On Unknown Text

Date preservation works by identifying components inside text. For example, in "the 21st of jan": "21st" is a day component, and "jan" is an abbreviated month. Since these are recognised as parts of a date, they are maskable components and are safe. This leaves the parts "the " and " of ". DataMasque has a set of safe strings it considers safe under all circumstances, which includes these parts. Specifically, the following are treated as safe:

  • whitespace
  • punctuation (commas, apostrophes etc) and symbols (@, # etc)
  • DataMasque's internal allow-list
  • anything you add via exempt - see below

DataMasque's internal allow-list contains exactly:

["at", "day", "early", "in", "late", "mid", "of", "on", "the", "time", "year"]

You can extend this allow-list by adding items to the exempt list. For example to allow "GMT" you can add it with: exempt: [gmt]. The exempt list is not case-sensitive.

To take a concrete example, a date with a misspelling, Aprril 15th, contains Aprril with two rs. DataMasque does not recognise this text so it is treated as unknown text. The action field then specifies what happens when unknown text like this is encountered.

Suppose the mask is from_random_date which generates a replacement of 2009-12-02 with the default fallback_date_format (see below).

Input action Output Behaviour
"Aprril 15th" remove " 2nd" Replaces the unknown text with an empty string
"Aprril 15th" treat_as_invalid "2009-12-02" Uses the fallback_date_format - see below
"Aprril 15th" retain "Aprril 2nd" allows unknown text to pass through unfiltered

WARNING: if non-date inputs are provided to a compatible mask, such as "John Smith was born on the 21st of Feb", retain causes sensitive data in those inputs to pass through unmasked. In this case "John Smith" remains in the output string. The same preservation of sensitive data occurs if "John" and/or "Smith" are on the exempt list and you set action: remove or action: treat_as_invalid.

Fallback Date Format

The fallback_date_format is used to specify the date format used under any of the following conditions:

  1. The parser has failed to parse the date.
  2. Unknown text is encountered and action: treat_as_invalid is set.
  3. The value is null.

Note: with a date mask, a masking run never fails if the parser fails. See typecast Without a Date Mask.

Examples

To override the default fallback_date_format of "%Y-%m-%d" you can set fallback_date_format to "%m.%d.%Y":

- column: unstructured_dates
  date_format_preservation:
    enabled: true
    fallback_date_format: "%m.%d.%Y"
  hash_columns:
    - constant_column     # used here only so `from_random_date` generates the same date for every row
  masks:
    - type: from_random_date
      min: "1980-01-01"
      max: "2000-01-01"

Suppose from_random_date generated 1999-02-08. Then example output may look like:

Input Output
"March the 22nd, 2005" "February the 8th, 1999"
N/A "02.08.1999"
"" "02.08.1999"

Usage

Expand the rulesets below for examples.

mask_table and mask_tabular_file

Show Ruleset

The settings can be set at the column level, or under a label inside unstructured_text.

version: "1.0"
tasks:
  - type: mask_table
    table: patients
    key: id
    rules:
      - column: notes
        date_format_preservation:
          enabled: true
        masks:
          - type: unstructured_text
            matchers:
              ai_detect:
                - label: DATE
            masks:
              - label: DATE
                date_format_preservation:
                  date_order: day_first
                masks:
                  - type: from_random_date
                    min: "1980-01-01"
                    max: "2000-01-01"

mask_file

Show Ruleset

mask_file has no column, but the settings on the label are supported.

version: "1.0"
tasks:
  - type: mask_file
    rules:
      - masks:
          - type: unstructured_text        # preservation is enabled by default here
            matchers:
              ai_detect:
                - label: DATE
            masks:
              - label: DATE
                date_format_preservation:
                  date_order: day_first
                masks:
                  - type: from_random_date
                    min: "1980-01-01"
                    max: "2000-01-01"

json and xml

Show Ruleset

Preservation can be set at the column level for both json and xml.

version: "1.0"
tasks:
  - type: mask_table
    table: patients
    key: id
    rules:
      - column: json_data
        date_format_preservation:
          enabled: true
        masks:
          - type: json
            transforms:
              - path:
                  - dob
                masks:
                  - type: from_random_date
                    min: "1980-01-01"
                    max: "2000-01-01"

      - column: xml_data
        date_format_preservation:
          enabled: true
        masks:
          - type: xml
            transforms:
              - path: "//Patient"
                node_transforms:
                  - type: text
                    masks:
                      - type: from_random_date
                        min: "1980-01-01"
                        max: "2000-01-01"

Note: date_format_preservation is only compatible with string values. In a json document, 19780314 (with no quotes) or a boolean are not parsed as strings and will fail the run. In this case, you should typecast using typecast_as: string before applying date masks.

How Settings Cascade

A date_format_preservation block can sit in two places: on a column rule, and under a label inside unstructured_text. The label is more specific, so if both state the same field, the label wins. A mask_file rule has no column, so a label is the only place its settings can go.

Settings merge field by field, not by block. A label that states only date_order still takes fallback_date_format from the column. An unspecified field takes its default.

enabled is the one field whose default depends on where it is read. A mask under unstructured_text preserves the date format unless explicitly disabled. Everywhere else, preservation is disabled unless it's turned on.

Column rule Label rule Preservation for masks under a label under unstructured_text
nothing nothing enabled
enabled: false nothing disabled
enabled: false enabled: true enabled, and the label still inherits the column's other settings
enabled: true enabled: false disabled

For the examples below, suppose the mask generates 2005-10-07 for the DATE_OF_BIRTH entity and 2026-03-14 for the DATE entity.

Set Once for Multiple Entities

Show Example

The following ruleset sets enabled: true which is then inherited by the masks under the DATE and DATE_OF_BIRTH labels. The treat_as_invalid action is also inherited in a similar manner. This means that if there is a DATE or DATE_OF_BIRTH entity which fails to parse, the default fallback date format is used.

- column: clinical_notes
  date_format_preservation:
    enabled: true
    on_unknown_text:
      action: treat_as_invalid
  masks:
    - type: unstructured_text
      matchers:
        ai_detect:
          - label: DATE
          - label: DATE_OF_BIRTH
      masks:
        - label: DATE
          masks:
            - type: from_random_date
              ...
        - label: DATE_OF_BIRTH
          masks:
            - type: from_random_date
              ...

Example output:

Input Output
Patient born on the 23rd of feb. Admitted Aprril the 9th. Patient born on the 7th of oct. Admitted 2026-03-14.

Neither label set a fallback_date_format, so both take the column's. The date of birth preserves the format. Aprril is unknown text, action is treat_as_invalid, so the entry is replaced using the default fallback_date_format.

Override an Inherited Setting for One Entity

Show Example

The following ruleset sets enabled: true for the column and this setting is inherited by both DATE and DATE_OF_BIRTH labels. We can then overwrite the fallback_date_format for the DATE_OF_BIRTH label individually.

- column: clinical_notes
  date_format_preservation:
    enabled: true
    fallback_date_format: "%Y.%m.%d"
  masks:
    - type: unstructured_text
      matchers:
        ai_detect:
          - label: DATE
          - label: DATE_OF_BIRTH
      masks:
        - label: DATE
          masks:
            - type: from_random_date
              ...
        - label: DATE_OF_BIRTH
          date_format_preservation:
            fallback_date_format: "%d_%m_%Y"
          masks:
            - type: from_random_date
              ...

Suppose, for illustrative purposes, that both 52-15-1997 and last night are detected as dates in the example below:

Input Output
Patient DOB: 52-15-1997. Admitted last night. Patient DOB: 07102005. Admitted 2026.03.14.

Neither value gives the parser a date. 52-15-1997 is not a valid date, and last night is not a date. The parser declines both, so each one takes a fallback format. The date of birth takes the label's %d_%m_%Y. The admission date takes the column's %Y.%m.%d, because the DATE label states none.

Disable the unstructured_text Default

Show Example

The following ruleset sets enabled: false for the column, which disables date format preservation for every mask under all labels below it. A label that disables date format preservation needs a typecast after the date mask.

Note: typecast is not required when date format preservation is enabled.

- column: clinical_notes
  date_format_preservation:
    enabled: false
  masks:
    - type: unstructured_text
      matchers:
        ai_detect:
          - label: DATE
      masks:
        - label: DATE
          masks:
            - type: from_random_date
              ...
            - type: typecast
              typecast_as: string

Note that the ruleset only detects DATE entities here, so the replacement date is 2026-03-14 for both for illustrative purposes.

Input Output
Patient born on 23 feb. Admitted 03/04/2025. Patient born on 2026-03-14. Admitted 2026-03-14.

No formats are preserved because date format preservation is disabled. By default, typecast converts a date to a string using the %Y-%m-%d format, which is how the dates come out in the provided example.

Note if structured outputs like this are required, you can also specify the output format using typecast, as described in Interaction with typecast.

Times and Timezones

The general principle for date_format_preservation is dates retain their formats, but if the input also contains a time/timezone then these are passed through unmasked. If times must be masked, do not use date_format_preservation.

However, this is conditional on there being recognisable date components to begin with. If no date components can be identified, the parser considers this a parse failure and falls back to the Fallback Date Format. See the last row of the table below for an example.

For example with a generated date 2007-07-05:

Input Output Parser Succeeds
"2001/03/15 14:32:17" "2007/07/05 14:32:17" Yes
"Jan the 3rd at 5pm" "Jul the 5th at 5pm" Yes
"Sat, 28 Jul 1956 10:41:31 +1300" "Thu, 5 Jul 2007 10:41:31 +1300" Yes
"2001-03-12 14:32 UTC+0245" "2007-07-05 14:32 UTC+0245" Yes
"14:32:17 +1300" "2007-07-05" No - not a date
"3pm" "2007-07-05" No - not a date

Recognised timezones

Note that timezones are recognised in any case, such as GMT or gmt.

ACDT, ACST, ACT, ACWST, ADT, AEDT, AEST, AET, AFT, AKDT, AKST, ALMT, AMST, AMT, ANAT, AQTT, ART,
AST, AWST, AZOST, AZOT, AZT, BIOT, BIT, BNT, BOT, BRST, BRT, BST, BTT, CAT, CCT, CDT, CEST, CET,
CHADT, CHAST, CHOST, CHOT, CHST, CHUT, CIST, CKT, CLST, CLT, COST, COT, CST, CT, CVT, CWST, CXT,
DAVT, DDUT, DFT, EASST, EAST, EAT, ECT, EDT, EEST, EET, EGST, EGT, EST, ET, FET, FJT, FKST, FKT,
FNT, GALT, GAMT, GET, GFT, GILT, GIT, GMT, GST, GYT, HAEC, HDT, HKT, HMT, HOVST, HOVT, HST, ICT,
IDLW, IDT, IOT, IRDT, IRKT, IRST, IST, JST, KALT, KGT, KOST, KRAT, KST, LHST, LINT, MAGT, MART,
MAWT, MDT, MEST, MET, MHT, MIST, MIT, MMT, MSK, MST, MT, MUT, MVT, MYT, NCT, NDT, NFT, NOVT, NPT,
NST, NT, NUT, NZDST, NZDT, NZST, OMST, ORAT, PDT, PET, PETT, PGT, PHOT, PHST, PHT, PKT, PMDT, PMST,
PONT, PST, PT, PWT, PYST, PYT, RET, ROTT, SAKT, SAMT, SAST, SBT, SCT, SDT, SGT, SLST, SRET, SRT,
SST, SYOT, TAHT, TFT, THA, TJT, TKT, TLT, TMT, TOT, TRT, TST, TVT, ULAST, ULAT, UT, UTC, UYST, UYT,
UZT, VET, VLAT, VOLT, VOST, VUT, WAKT, WAST, WAT, WET, WGST, WGT, WIB, WIT, WITA, WST, YAKT, YEKT, Z

Interaction with typecast

With date format preservation enabled, compatible masked dates are automatically coerced to a string when required. As such, a typecast mask is NOT required after a compatible mask.

However, typecast can be used to coerce an unstructured masked date into a structured format if you desire.

Note: With date format preservation on, a typecast with a date_format overrides the preserved shape. If date_format is excluded, a typecast with typecast_as: string has no effect.

How to Coerce Unstructured Dates to a Structured Format

Show Example

If you add typecast after a from_random_date mask, the typecast takes priority and coerces the date into the specified format.

version: "1.0"
tasks:
  - type: mask_table
    table: patients
    key: id
    rules:
      - column: notes
        masks:
          - type: unstructured_text
            matchers:
              ai_detect:
                - label: DATE
            masks:
              - label: DATE
                date_format_preservation:
                  enabled: true
                masks:
                  - type: from_random_date
                    ...
                  - type: typecast
                    typecast_as: string
                    date_format: "%d.%m.%Y"

Example output:

Before After
Patient DOB: 3rd of January 1997. Admitted 14th of March 2020. Patient DOB: 04.04.1988. Admitted 21.08.2010.
Born 8th May 1979. Follow-up scheduled for September the 3rd, 2021. Born 15.11.1987. Follow-up scheduled for 15.08.2015.

Note: In an unstructured_text label, if typecasting to date or datetime, make sure you typecast back to a string at the end. Otherwise, the run fails.

typecast Without a Date Mask

If date_format_preservation is enabled, the input text will be parsed using the date_format_preservation parser.

If the input is missing fields, a typecast to string with a date_format will fail the run. This is because for typecast to convert a parsed value to a structured date, all fields must be present.

Warning: It is not recommended to use typecast without a date mask with date_format_preservation enabled.

Show Example

This rule writes each date in the %Y-%m-%d format:

- column: unstructured_dates
  date_format_preservation:
    enabled: true
  masks:
    - type: typecast
      typecast_as: string
      date_format: "%Y-%m-%d"
Input Output
14 March 1978 1978-03-14
03/04/2025 2025-03-04
March 2024 Run fails - value is missing a day, a month or a year
02/31/2024 Run fails - value is not a valid date
John Smith Run fails - value is not a date

Things to Know

  • If a weekday exists in the input, the weekday changes to the true weekday of the masked date.
  • A two-digit year uses a fixed pivot at 69. A year below 69 becomes 20xx, and a year of 69 or more becomes 19xx. So '50 becomes 2050, and '75 becomes 1975. This matches how DataMasque parses years without a century.
  • Sept becomes Sep, Tues becomes Tue, and Thurs becomes Thu.

Compatibility

If date_format_preservation is enabled:

  • chain: masks run in order, each reading the output of the one before. A date mask keeps the shape of the text it receives.
  • concat: each mask inside concat reads the input concat receives, so a date mask inside a concat preserves the format, regardless of the siblings.
  • typecast: can be used to convert a date to a string, date, or datetime. Note that typecasting to date or datetime does not preserve the format. See Interaction with typecast.

The following are not compatible:

  • imitate_date mask.
  • from_random_date mask with epoch parameters. This applies to the whole column, so a json or xml mask cannot use epoch parameters in one transform and date format preservation in another.
  • Wildcard column rules (column: "*"), when masking with Amazon DynamoDB and MongoDB.
  • Number and boolean values in a json document.

Specifying a Date Format

Often, rulesets need to specify a date format to convert text into a date. For example, using Date/Time masks, executing type casts or performing Conditional Masking.

Date Format Directives

Date parsing in DataMasque follows standard %-based directives, as documented in strftime() and strptime() Format Codes.

An important point to note is how years with and without centuries are handled.

Parsing Years with/without Centuries (%Y vs. %y)

Two directives are available to parse years:

  • %Y to parse years with the century (e.g., 2020, 1999, 1867, etc.).
  • %y to parse years without the century (e.g., 20, 99, 67, etc.).

When parsing years without a century using %y, the following interpretation applies:

  • Years from 00 to 68 are interpreted as the years 2000 to 2068.
  • Years from 69 to 99 are interpreted as the years 1969 to 1999.

While %y provides a convenient way to parse two-digit years, it introduces ambiguity in interpretation, especially near the transition between centuries.

It is recommended to use %Y whenever possible to avoid ambiguity, and ensure consistent interpretation across platforms and environments.

Related Topics