DEV Community

Cover image for Find, convert and replace dates with Vim substitutions
Jeff Kreeftmeijer
Jeff Kreeftmeijer

Posted on • Originally published at jeffkreeftmeijer.com

Find, convert and replace dates with Vim substitutions

Originally published at jeffkreeftmeijer.com/vim-reformat-dates on Oct 17, 2017.

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.

Finding, converting and replacing dates with Vim<br>
substitutions

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

<article>
  <h1>Keeping open source projects maintainable</h1>
  <time datetime="2017-09-19">2017-09-19</time>
</article>

<article>
  <h1>Property-based testing in Elixir using PropEr</h1>
  <time datetime="2017-08-22">2017-08-22</time>
</article>

<article>
  <h1>git is not a git command</h1>
  <time datetime="2015-10-01">2015-10-01</time>
</article>

...
Enter fullscreen mode Exit fullscreen mode

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

<article>
  <h1>Keeping open source projects maintainable</h1>
  <time datetime="2017-09-19">September 19, 2017</time>
</article>

<article>
  <h1>Property-based testing in Elixir using PropEr</h1>
  <time datetime="2017-08-22">August 22, 2017</time>
</article>

<article>
  <h1>git is not a git command</h1>
  <time datetime="2015-10-01">October 01, 2015</time>
</article>

...
Enter fullscreen mode Exit fullscreen mode

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.

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.

<time datetime="2017-09-19">2017-09-19</time>
Enter fullscreen mode Exit fullscreen mode

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.

....-..-..<
Enter fullscreen mode Exit fullscreen mode

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<
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode
  • -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.

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
Enter fullscreen mode Exit fullscreen mode
  • ....-..-..\ze<: 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.

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
Enter fullscreen mode Exit fullscreen mode
  • ....-..-..\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.

⚠️ This substitution produces a newline in 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.

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
Enter fullscreen mode Exit fullscreen mode

Running this substitution will turn all <time> tags from the input file to our desired format, but it beaks the closing </time> tag with an extra newline.

The current result, with an added newline before the closing </time> tag

<article>
  <h1>Keeping open source projects maintainable</h1>
  <time datetime="2017-09-19">September 19, 2017<
/time>
</article>

...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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", "")
Enter fullscreen mode Exit fullscreen mode
  • "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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now our substitution will turn the <time> tags into the correct format, without adding that extra newline.

The result

<article>
  <h1>Keeping open source projects maintainable</h1>
  <time datetime="2017-09-19">September 19, 2017</time>
</article>

<article>
  <h1>Property-based testing in Elixir using PropEr</h1>
  <time datetime="2017-08-22">August 22, 2017</time>
</article>

<article>
  <h1>git is not a git command</h1>
  <time datetime="2015-10-01">October 01, 2015</time>
</article>

...
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
moopet profile image
Ben Sinclair

This is really cool.

Worth pointing out that relying on external commands is not portable (in this case it's specific to BSD date) and that you could also pipe the system command through | tr -d "\n" to get rid of any trailing newlines.

Collapse
 
jkreeftmeijer profile image
Jeff Kreeftmeijer

Hey Ben, 👋

Thanks for your kind words!

There’s some more about the differences between BSD and GNU date in the original. You’re right, though; using external dependencies makes this less portable.

I used the nested substitution to show another crazy thing you can do within a substitution. Piping the result from the date command through tr is probably easier to remember, so I'll add a quick note about that. Thanks!

Collapse
 
voyeg3r profile image
Sérgio Araújo

Sorry @dev.to/jkreeftmeijer, I have not fount this article before publishing mine, because I stumbled upon your post here: jeffkreeftmeijer.com/vim-reformat-... but my post is more Linux oriented as you can see here: dev.to/voyeg3r/formating-dates-wit... it was inspired on yours but has little differences.