Find, convert and replace dates with Vim substitutions
source link: https://jeffkreeftmeijer.com/vim-reformat-dates/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Find, convert and replace dates with Vim substitutions
Find, convert and replace dates with Vim substitutions
By Jeff Kreeftmeijer on 2017-10-17, last updated on 2021-01-07
Vim’s substitution command is a powerful way to make changes to text files.
Besides finding and replacing text using regular expressions, substitutions can
call out to external programs for more complicated replacements. By using the
date
utility from a substitution, Vim can convert all dates in a file to a
different format and replace them all at once.
The input file is an HTML page with a list of articles. Each article includes
a <time>
tag with a value and a datetime attribute to show the publication
date.
The input file
We need to convert the dates’ values to a friendlier format that includes the full month name (“September 19, 2017”), while keeping the datetime attributes in their current format.
The result: articles with reformatted dates
The input file has more than forty articles, so replacing them all by hand would be a lot of error-prone work. Instead, we write a substitution that finds all dates in the file and replaces them with a reformatted value.
Finding the dates
The first step in replacing the dates is finding where they are in the input file and making sure not to match the ones in the datetime attributes.
Finding patterns
Vim searches for text by pressing esc to get to normal mode, then
/, followed by a search pattern. Type esc /2017
to find all
occurrences of "2017" in a file.
The dot (.
) is a wildcard that matches any single character. Searching for
20..
(esc /20..
) matches “2017”, but also “2015”, “2078”,
“20a%”, and “20°c”.
To find all dates in the file, we could use ....-..-..
(esc
/....-..-..
) as our search pattern to match the date format. However, this
pattern’s results will include all matching dates in the file, including the
ones in the <time>
tags’ datetime attributes.
In the input file, all <time>
values are immediately followed by the
less-than sign from the closing </time>
tag. To prevent the datetimes from
the attributes to be included in the results, we could include the less-than
sign in the search pattern and make sure to add it back when replacing.
....-..-..<
Hoever, Vim supports setting the start and end of the match in the search
pattern using the \zs
and \ze
pattern atoms. By prefixing the <
in our
search pattern with \ze
, the pattern finds all dates followed by a less-than
sign, but doesn’t include it in the match, meaning it won’t be replaced.
....-..-..\ze<
Reformatting dates from the command line
We need the month’s full name in the date replacements, so we can’t reorder the input value (“2017-09-19”) to get the result we want. Instead, we need to call out to an external utility that knows month names and can convert between date formats.
The date
utility
The date
utility returns the current date and time. The desired output
format is passed in through a trailing argument that starts with a plus sign.
It needs to be wrapped in quotes if it contains any whitespace.
$ date Thu Oct 17 09:41:10 CEST 2017 $ date +%Y-%m-%d 2017-10-17 $ date +"%B %d, %Y" October 17, 2017
On macOS (or other derivatives of BSD) passing a date to the date
utility
overwrites the system date unless the -j
flag is passed. With that flag, the
utility allows passing a test date to be reformatted. The test date needs to be
formatted as [[[mm]dd]HH]MM[[cc]yy][.ss]
by default or we can pass a custom
input format with -f
.
$ date -j110200361991.35 Sat Nov 2 00:36:35 CET 1991 $ date -jf %Y-%m-%d 1991-11-02 Sat Nov 2 09:41:27 CET 1991
GNU date
uses the --date
argument to pass in dates, and it figures out the
input format on its own.
# date --date=1991-11-02 Sat Nov 2 00:00:00 UTC 1991
By setting the input and output format, we can use the date
utility to
reformat dates.
$ date -jf %Y-%m-%d 1991-11-02 +"%B %d, %Y" # BSD November 02, 1991
# date --date=1991-11-02 +"%B %d, %Y" # GNU November 02, 1991
We reformat each match of our search pattern to our desired format (“September
19, 2017”) with the date
utility. We use "%Y-%m-%d"
as the input format
to match the results from the search pattern. The output format is "%B %d,
%Y"
to produce the month’s full name, the date’s number, a comma and the year
number.
With these formats the date
utility reformats 1991-11-02
to November 02,
1991
.
$ date -jf "%Y-%m-%d" "1991-11-02" +"%B %d, %Y" November 02 1991
-j
Don’t try to set the system date-f "%Y-%m-%d"
Use the passed input format instead of the default. In this case "%Y-%m-%d"
to match the input format (1991-11-02
)."1991-11-02"
An example date to be parsed using the input format passed to -f
.+"%B %d, %Y"
The output format, which produces November 02, 1991
.Calling out to external utilities from substitutions
We know how to find all dates in the file, and how to convert a date to another
format from the command line. To replace all found dates with a reformatted
version from the date
utility, we need to run an expression from a
substitution.
Substitutions
Vim’s substitutions can find and replace text. To replace “November” with
“October” throughout a file, we execute a substitution where the pattern
is November
and the substitute string is October
.
:%s/November/October/gc
Note:
Options can be passed after the last slash. In this case, we use
g[lobal]
to make the substitution global, meaning it will replace all
occurrences of the pattern in the file. The second option is c[onfirm]
, which
will ask for confirmation before executing each substitution.
Using the search pattern we prepared earlier, we can find and replace all date values from the input file with a substitution. For example, we could overwrite all dates with a hardcoded value:
:%s/....-..-..\ze</November 2, 1991/gc
....-..-..
The search pattern to find all dates in the file.November 2, 1991
The literal substitute string to replace the dates with a hardcoded one.Instead of inserting a hardcoded substitute string, we need to run an expression for each match to get its replacement.
Expressions
When a substitute string starts with \=
, Vim evaluates it as an expression.
We can call Vim’s built in functions from an expression. To replace all numbers
in a file with the number of the line they’re on, we use the line()
function
from an expression in the substitute string.
%s/\d\+/\=line('.')/gc
\d\+
The pattern to match all numbers (\d
) in the file. Multi-digit numbers (42,
785, 14281) are matched as one number by using \+
.\=line('.')
The substitute string with an expression (\=
) to call the line()
function. Passing '.'
as the function’s argument returns the current cursor
position, which is used to replace the match.Vim provides the system()
function to call out to an external command and use
the result as the replacement string. To replace all numbers in a file with a
random number, we call echo -n $RANDOM
with the system()
function.
:%s/\d\+/\=system('echo -n $RANDOM')/gc
We use the system()
function from an expression (\=
) to call out to the
date
utility. Sticking with hardcoded dates for now, we can use the utility
to convert a date’s format from “1991-11-02” to “November 2, 1991” before
inserting it into the file:
:%s/....-..-..\ze</\=system('date -jf "%Y-%m-%d" "1991-11-02" +"%B %d, %Y"')/gc
....-..-..\ze<
The search pattern to find all dates in the file.\=system('date …')
An expression that uses the system()
function to execute an external
command and returns its value as the substitute string.'date -jf "%Y-%m-%d" "1991-11-02" +"%B %d, %Y"'
The date command as a string, with a hardcoded date
("1991-11-02"
) as its input date argument. This date matches the format of
the search pattern’s matches.Warning:
This substitution produces a newline before the <time>
tag, because
the date
utility appends one to its output. We’ll remove these later
while discussing nested substitutions.
The replacement value is still hardcoded (“1991-11-02”), so this substitution will overwrite all date values in the file to a date in 1991. To put the matched date values back in the file, we need to pass them to the date command.
Submatches
Vim’s submatch()
function returns matches from our pattern. If we call it
with 0
as its argument, it will return the whole match instead of a submatch.
To wrap each occurrence of “October” in brackets, we use [\0]
as the
substitute string.
:%s/October/[\0]/gc
In an expression, submatches can be included using the submatch()
function.
:%s/October/\='['.submatch('0').']'/gc
To pass the matched date to the call to date
in our expression, we need to
break out of the string passed to the system()
function and replace the
hardcoded date with a call to submatch(0)
to insert the whole match.
:%s/....-..-..\ze</\=system('date -jf "%Y-%m-%d" "'.submatch(0).'" +"%B %d, %Y"')/gc
Running this substitution will turn all <time>
tags from the input file to
our desired format, but it puts a newline before the closing </time>
tag.
The current result, with an added newline before the closing </time>
tag
Nested substitutions
A newline is appended to the result of the date
command, which ends up in the
file after running the substitution. Since there’s no way to get the date
command to omit the newline, we need to take it out ourselves.
We can run a second substitution to remove them after running the first one:
:%s/\n<\/time>/<\/time>/g
To keep the original substitution from adding newlines in the first place, we
can pipe the result from the date
command to tr
to remove the newline with
the -d
argument:
:%s/....-..-..\ze</\=system('date -jf "%Y-%m-%d" "'.submatch(0).'" +"%B %d, %Y" | tr -d "\n"')/gc
Another option is to take the newline out with a nested substitution.
The substitute()
function
Vim’s substitute()
function replaces strings and can be run from an
expression in a substitution. Nested substitutions are useful for transforming
the result of another function.
The function (substitute()
) works like the substitute command (:s
), and
takes the same arguments, so substitute("input", "find", "replace", "g")
is
equivalent to running :%s/find/replace/g
in a file.
The substitute()
function works like the substitute command (:s[ubstitute]
)
in Vim’s command line and takes the same arguments. The first argument is the
input, then the search pattern, the substitute string, followed by optional
options. substitute("input", "find", "replace", "g")
is equivalent to running
:%s/find/replace/g
in a file.
:echom substitute("October 02, 1991", "October", "November", "")
"October 02, 1991"
The input string to run the substitution on."October"
The search pattern."November"
The substitute string.""
Options, like in a “regular” substitution. This example
doesn’t use the g
option because we’re sure there’s only one match in the
input string, so it isn’t necessary.If an external command called from a substitution returns a trailing newline
(like echo
would without the -n
flag), we can use the substitute()
function to take it out before the match is replaced.
:%s/October/\=substitute(system('echo "November"'), "\n", "", "")/gc
We wrap the call to the date
utility in a nested substitution using the
substitute()
function. It takes the result, matches the newline ("\n"
) and
replaces it with an empty string.
:%s/....-..-..\ze</\=substitute(system('date -jf "%Y-%m-%d" "'.submatch(0).'" +"%B %d, %Y"'), "\n", "", "")/gc
Now our substitution will turn the <time>
tags into the correct format,
without adding that extra newline.
The result
<ul> <li> <article> <time datetime="2017-10-17">October 17, 2017</time> <span><a href="/vim-reformat-dates/">Find, convert and replace dates with Vim substitutions</a></span> </article> </li> <li> <article> <time datetime="2017-09-19">September 19, 2017</time> <span><a href="/open-source-maintainability/">Keeping open source software projects maintainable</a></span> </article> </li> <li> <article> <time datetime="2017-08-22">August 22, 2017</time> <span><a href="/mix-proper/">Property-based testing in Elixir using PropEr</a></span> </article> </li> …
Thanks Wouter Vos and Rico Sta. Cruz for feedback on the substitution command
styling, u/Vurpius for suggesting \ze
and Ben Sinclair for suggesting piping
through tr
.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK