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
- Specifying a Date Format
- Related Topics
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_datemask 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_preservationreads dates out of text, so use it on text columns only. Withdate_format_preservationenabled,from_random_datefails the run when it receives a date or datetime from aDATEorTIMESTAMPcolumn or from a previous mask.
enabled(optional): Turn date format preservation on or off. Defaults totruefor anunstructured_textmask label andfalseeverywhere else. See How Settings Cascade.date_order(optional): Which number is the day when a date reads two ways, such as03/04/25. One ofday_firstormonth_first. Defaults tomonth_first.on_unknown_text(optional): What to do with a word beside the date that the parser does not know.actioncan be one ofretain,remove, ortreat_as_invalid, defaults toremove.exemptdefaults 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 theday_firstrule - 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",
retaincauses 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 theexemptlist and you setaction: removeoraction: treat_as_invalid.
Fallback Date Format
The fallback_date_format is used to specify the date format used
under any of the following conditions:
- The parser has failed to parse the date.
- Unknown text is encountered and
action: treat_as_invalidis set. - The value is null.
Note: with a date mask, a masking run never fails if the parser fails. See
typecastWithout 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_preservationis only compatible with string values. In ajsondocument,19780314(with no quotes) or a boolean are not parsed as strings and will fail the run. In this case, you shouldtypecastusingtypecast_as: stringbefore 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:
typecastis 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
typecastwith adate_formatoverrides the preserved shape. Ifdate_formatis excluded, atypecastwithtypecast_as: stringhas 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_textlabel, if typecasting todateordatetime, make sure you typecast back to astringat 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
typecastwithout a date mask withdate_format_preservationenabled.
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
'50becomes2050, and'75becomes1975. This matches how DataMasque parses years without a century. SeptbecomesSep,TuesbecomesTue, andThursbecomesThu.
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 insideconcatreads the inputconcatreceives, so a date mask inside aconcatpreserves the format, regardless of the siblings.typecast: can be used to convert a date to astring,date, ordatetime. Note that typecasting todateordatetimedoes not preserve the format. See Interaction withtypecast.
The following are not compatible:
imitate_datemask.from_random_datemask with epoch parameters. This applies to the whole column, so ajsonorxmlmask 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
jsondocument.
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:
%Yto parse years with the century (e.g., 2020, 1999, 1867, etc.).%yto 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
- Unstructured Text Masking
from_random_datemasktypecastmaskimitate_datemask