2

I've been reading up on dispatch tables and I get the general idea of how they work, but I'm having some trouble taking what I see online and applying the concept to some code I originally wrote as an ugly mess of if-elsif-else statements.

I have options parsing configured by using GetOpt::Long, and in turn, those options set a value in the %OPTIONS hash, depending on the option used.

Taking the below code as an example... (UPDATED WITH MORE DETAIL)

use     5.008008;
use     strict;
use     warnings;
use     File::Basename qw(basename);
use     Getopt::Long qw(HelpMessage VersionMessage :config posix_default require_order no_ignore_case auto_version auto_help);

my $EMPTY      => q{};

sub usage
{
    my $PROG = basename($0);
    print {*STDERR} $_ for @_;
    print {*STDERR} "Try $PROG --help for more information.\n";
    exit(1);
}

sub process_args
{
    my %OPTIONS;

    $OPTIONS{host}              = $EMPTY;
    $OPTIONS{bash}              = 0;
    $OPTIONS{nic}               = 0;
    $OPTIONS{nicName}           = $EMPTY;
    $OPTIONS{console}           = 0;
    $OPTIONS{virtual}           = 0;
    $OPTIONS{cmdb}              = 0;
    $OPTIONS{policyid}          = 0;
    $OPTIONS{showcompliant}     = 0;
    $OPTIONS{backup}            = 0;
    $OPTIONS{backuphistory}     = 0;
    $OPTIONS{page}              = $EMPTY;

    GetOptions
      (
        'host|h=s'              => \$OPTIONS{host}               ,
        'use-bash-script'       => \$OPTIONS{bash}               ,
        'remote-console|r!'     => \$OPTIONS{console}            ,
        'virtual-console|v!'    => \$OPTIONS{virtual}            ,
        'nic|n!'                => \$OPTIONS{nic}                ,
        'nic-name|m=s'          => \$OPTIONS{nicName}            ,
        'cmdb|d!'               => \$OPTIONS{cmdb}               ,
        'policy|p=i'            => \$OPTIONS{policyid}           ,
        'show-compliant|c!'     => \$OPTIONS{showcompliant}      ,
        'backup|b!'             => \$OPTIONS{backup}             ,
        'backup-history|s!'     => \$OPTIONS{backuphistory}      ,
        'page|g=s'              => \$OPTIONS{page}               ,
        'help'                  => sub      { HelpMessage(-exitval => 0, -verbose ->1)     },
        'version'               => sub      { VersionMessage()  },
      ) or usage;

    if ($OPTIONS{host} eq $EMPTY)
    {
        print {*STDERR} "ERROR: Must specify a host with -h flag\n";
        HelpMessage;
    }

    sanity_check_options(\%OPTIONS);

    # Parse anything else on the command line and throw usage
    for (@ARGV)
    {
        warn "Unknown argument: $_\n";
        HelpMessage;
    }

    return {%OPTIONS};
}

sub sanity_check_options
{
    my $OPTIONS     = shift;

    if (($OPTIONS->{console}) and ($OPTIONS->{virtual}))
    {
        print "ERROR: Cannot use flags -r and -v together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -r and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flags -r and -b together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{console}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -r and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{virtual}) and ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flags -v and -b together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{virtual}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -v and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{virtual}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -v and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{backup}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -b and -d together\n";
        HelpMessage;
    }
    elsif (($OPTIONS->{backup}) and ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flags -b and -n together\n";
        HelpMessage;
    }

    if (($OPTIONS->{nic}) and ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flags -n and -d together\n";
        HelpMessage;
    }

    if (($OPTIONS->{policyid} != 0) and not ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flag -p without also specifying -d\n";
        HelpMessage;
    }

    if (($OPTIONS->{showcompliant}) and not ($OPTIONS->{cmdb}))
    {
        print "ERROR: Cannot use flag -c without also specifying -d\n";
        HelpMessage;
    }

    if (($OPTIONS->{backuphistory}) and not ($OPTIONS->{backup}))
    {
        print "ERROR: Cannot use flag -s without also specifying -b\n";
        HelpMessage;
    }

    if (($OPTIONS->{nicName}) and not ($OPTIONS->{nic}))
    {
        print "ERROR: Cannot use flag -m without also specifying -n\n";
        HelpMessage;
    }

    return %{$OPTIONS};
}

I'd like to turn the above code into a dispatch table, but can't figure out how to do it.

Any help is appreciated.

Speeddymon
  • 496
  • 2
  • 20
  • 1
    Are the sets of conflicting options always pairs? Can you have situations where options `a`, `b`, and `c` cannot occur together but any two are OK? Before you can pick a representation you need to be sure your model can handle the logic you need in a general way. This is not an easy problem. – Jim Garrison Nov 15 '17 at 05:42
  • Don't use English, it's horribly slow and makes your code harder to read. – simbabque Nov 15 '17 at 16:45
  • Removed English module and changed ```$ARG```/```@ARG``` to ```$_```/```@_``` Added ```$EMPTY``` as I forgot I had it defined globally. – Speeddymon Nov 15 '17 at 17:01
  • @JimGarrison -- you are correct. The if-elsif-else does not explicitly account for 3 options that conflict (though it does account for that implicitly) As an example, using ```-h``` is required with all of the other options. But, using ```-h```, ```-r```, ```v```, all together is not allowed, while ```-h```, ```-r```, and ```-d``` is allowed. – Speeddymon Nov 15 '17 at 17:15
  • Since the host must be provided, it should be an argument, not an option. – ikegami Nov 15 '17 at 17:24
  • @ikegami this isn't a question about coding style... I prefer anything after the command to have a flag preceding it. That's my preference. Therefore, I am keeping it as an option. I may, one day later on, make this so that it can take a text file with a list of hosts, at which time I would remove the requirement of ```-h``` – Speeddymon Nov 15 '17 at 18:32
  • I know. That's why I didn't post my comment as an answer. /// If you want to be able to use a list of hosts in the future, it makes even less sense to use an option instead of argument. It's far easier to provide multiple values as arguments than as options. – ikegami Nov 15 '17 at 18:49
  • I don't understand how that could be. ```command -l filename``` and ```command -h hostname``` could both be easily accounted for. ```command filename``` and ```command hostname``` would seem to be vastly more difficult to account for. Trying to open a hostname as a file handle, for reading, just doesn't work. Unless I am misunderstanding your meaning. – Speeddymon Nov 15 '17 at 18:56

3 Answers3

3

I am not sure how a dispatch table would help since you need to go through pair-wise combinations of specific possibilities, and thus cannot trigger a suitable action by one lookup.

Here is another way to organize it

use List::MoreUtils 'firstval';

sub sanity_check_options
{
    my ($OPTIONS, $opt_excl) = @_;

    # Check each of 'opt_excl' against all other for ConFLict
    my @excl = sort keys %$opt_excl;
    while (my $eo = shift @excl) 
    {
        if (my $cfl = firstval { $OPTIONS->{$eo} and $OPTIONS->{$_} } @excl) 
        {
            say "Can't use -$opt_excl->{$eo} and -$opt_excl->{$cfl} together";
            HelpMessage();
            last;
        }
    }

    # Go through specific checks on
    # policyid, showcompliant, backuphistory, and nicName
    ...
    return 1;  # or some measure of whether there were errors
}

# Mutually exclusive options
my %opt_excl = (
    console => 'r', virtual => 'v', cmdb => 'c', backup => 'b', nic => 'n'
); 

sanity_check_options(\%OPTIONS, \%opt_excl);

This checks all options listed in %opt_excl against each other for conflict, removing the segments of elsif involving the (five) options that are mutually exclusive. It uses List::MoreUtils::firstval. The few other specific invocations are best checked one by one.

There is no use of returning $OPTIONS since it is passed as reference so any changes apply to the original structure (while it's not meant to be changed either). Perhaps you can keep track of whether there were errors and return that if it can be used in the caller, or just return 1.

This addresses the long elsif chain as asked, and doesn't go into the rest of code. Here is one comment though: There is no need for {%OPTIONS}, which copies the hash in order to create an anonymous one; just use return \%OPTIONS;


Comment on possible multiple conflicting options

This answer as it stands does not print all conflicting options that have been used if there are more than two, as raised by ikegami in comments; it does catch any conflicts so that the run is aborted.

The code is readily adjusted for this. Instead of the code in the if block either

  • set a flag as a conflict is detected and break out of the loop, then print the list of those that must not be used with each other (values %opt_excl) or point at the following usage message

  • collect the conflicts as they are observed; print them after the loop

  • or, see a different approach in ikegami's answer

However, one is expected to know of allowed invocations of a program and any listing of conflicts is a courtesy to the forgetful user (or a debugging aid); a usage message is printed as well anyway.

Given the number of conflicting options the usage message should have a prominent note on this. Also consider that so many conflicting options may indicate a design flaw.

Finally, this code fully relies on the fact that this processing goes once per run and operates with a handful of options; thus it is not concerned with efficiency and freely uses ancillary data structures.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • Updated the question to clarify. – Speeddymon Nov 15 '17 at 16:44
  • @Speeddymon Thank you, updated. This brings together checks of those five options which can't go one with another. The remaining few I leave to be checked one by one; "encoding" one or two possibilities in some all-encompassing system would just increase complexity (and may end up less readable). – zdim Nov 15 '17 at 18:43
  • 1
    @Speeddymon Added the missing include, `use List::MoreUtils 'firstval'`. Edited a little in the meanwhile, as well. – zdim Nov 15 '17 at 20:55
  • 1
    Thank you for the easy to follow example. I went with yours as it was the clearest and contained the least duplicate code. – Speeddymon Nov 15 '17 at 23:49
  • @Speeddymon, Apparently, it's not clear as you think since you didn't realize if doesn't work. It doesn't mention the error of using `-r` and `-c` together if `-b` is also provided. And why is a hash being used at all? Wasteful and needlessly complex. – ikegami Nov 16 '17 at 06:28
  • Also, it does exactly the opposite of an dispatch table! The goal of a dispatch table is to turn an O(N) problem into an O(1) problem, but zdim turned the O(N) problem into an O(N^2) problem!!! – ikegami Nov 16 '17 at 06:37
  • @ikegami I don't understand any of this. What "_O(N^2) problem!!!_" -- this is command-line processing -- it happens _once_ per run. The dispatch table has no utility here, as _you also state_. As for multiple conflicts I follow the OP, where only pairs are checked. You accumulate all errors in your answer, what I think is great, but this does catch all invocation errors. Why do you say "it doesn't work" -- that's incorrect; any conflicting options _are_ caught. What is the rationale for these comments and for the -1, please? It is correct code that catches wrong invocation. – zdim Nov 16 '17 at 09:26
  • @ikegami If they want to catch _all_ that's wrong on the command line (what is of course good to do, while not necessary) they only need to remove `last;`. As for the hash, I don't understand -- you use the same data in your answer (it's just helpful for this); why do you call it wasteful in my answer? The hash is used since here it reduces prints to _one_. – zdim Nov 16 '17 at 09:51
  • @zdim, Re "*they only need to remove `last;`*", Nope. `HelpMessage` exits. Even if it didn't exit, you'd end up with 2 sucky error messages instead of one. This is just one example of the numerous problems with the OP's code that you completely ignored. /// Re "*you use the same data in your answer*", Yes, but I didn't needlessly store it in a hash instead of an array. /// Re "*The hash is used since here it reduces prints to one.*", No, it doesn't. There's only one print in mine too, and there's no hash. – ikegami Nov 16 '17 at 14:32
  • Re "*What is the rationale for these comments and for the -1, please?*", On top of those specific problems, It's just overall bad code. You actually increased the code complexity and the time complexity when the question is about obtaining the opposite. – ikegami Nov 16 '17 at 14:39
  • @ikegami If you'll recall, the question I answered had none of these "_numerous problems_." Once the rest showed I decided to keep the focus on answering the actual question, for one because the added details don't convey everything. Re: "_that you completely ignore_" -- incorrect, I didn't ignore them, I state explicitly what I am addressing. This is perfectly reasonable; the default in fact. Again, the "rest of the code" (quoting my post) doesn't show the full picture so addressing it is a specific decision (and then one takes risks for details that aren't shown). – zdim Nov 17 '17 at 00:15
  • @ikegami Re: "_Nope. .._". But just move that message out and use a flag or store messages or ... it only needs a minuscule change to posted code. That's what the hash you so don't like provides -- the code is adjusted easily. (As it was for a sweeping change to the OP.) But I _didn't want_ to do that. You did and I like how it's done now, but that's your answer. ... – zdim Nov 17 '17 at 00:16
  • @ikegami ... The point here is to not allow the program to run with conflicting options. Users are expected to be aware of invocation rules, and there's the full usage message printed. There is absolutely nothing wrong with code that catches errors and doesn't re-iterate the following usage message. – zdim Nov 17 '17 at 00:17
  • @ikegami The "_increased code complexity_" of one clean hash and a few lines of processing? Increased compared to ... a dozen-ish of `elsif` blocks with repeated prints? Or to a map with a ternary operator over a list of arrayref-pairs? (Again: that's great code, but why is it _simpler_ than a hash with dequeue?) The "_increased time complexity_" in the processing of command line options, and "_O(N^2)_" on **5** (five) elements, once per run? ... – zdim Nov 17 '17 at 00:17
  • @ikegami ... Re: "_**just overall** bad code_" (my emphasis). You should know full well that I respect everything you say. But "just overall", with the above arguments, borders on offensive. I sure am capable of putting out crappy code but this (answer to this question) _is not_. – zdim Nov 17 '17 at 00:17
  • @ikegami Added a comment on missed multiple conflicts, thank you for that. Never mind my last (now deleted) comment above, if you saw it; I just don't get all this. – zdim Nov 17 '17 at 05:58
  • @zdim, No, compared to the code whose complexity you were asked to *reduce to O(1)*. Again, I remind you what the question was. /// Yes, it's bad when there are five problems in seven lines plus leaving another six issues uncorrected. – ikegami Nov 17 '17 at 15:41
  • @ikegami The question is about "_something simpler_", to clean up those `elsif`, and they just thought of dispatch tables to reduce it to one statement; they never mentioned nor hinted at performance. If you keep saying that you see "five" problems there I guess that's that; I stated what I think about each claim. Btw, I timed it: your code is around 20 **microseconds** faster, and here the matter _is_ of absolute time since this goes once per run, and always with data of roughly this size (rough average, and that's after I adjust my code for multiple conflicts, otherwise it's about 10 us). – zdim Nov 17 '17 at 19:50
  • Re "*they never mentioned nor hinted at performance*", They mentioned a dispatch table. The benefit of a dispatch table is performance. /// Re "*The question is about "something simpler",*", Yet you didn't provide something simpler; you provided something that's still as buggy as the original, and doesn't provide a way of fixing them. – ikegami Nov 17 '17 at 19:53
0

You shouldn't be using elsif here because multiple condition could be true. And since multiple conditions could be true, a dispatch table can't be used. Your code can still be simplified greatly.

my @errors;

push @errors, "ERROR: Host must be provided\n"
   if !defined($OPTIONS{host});

my @conflicting =
   map { my ($opt, $flag) = @$_; $OPTIONS->{$opt} ? $flag : () }
      [ 'console', '-r' ],
      [ 'virtual', '-v' ],
      [ 'cmdb',    '-d' ],
      [ 'backup',  '-b' ],
      [ 'nic',     '-n' ];

push @errors, "ERROR: Can only use one the following flags at a time: @conflicting\n"
   if @conflicting > 1;

push @errors, "ERROR: Can't use flag -p without also specifying -d\n"
   if defined($OPTIONS->{policyid}) && !$OPTIONS->{cmdb};

push @errors, "ERROR: Can't use flag -c without also specifying -d\n"
   if $OPTIONS->{showcompliant} && !$OPTIONS->{cmdb};

push @errors, "ERROR: Can't use flag -s without also specifying -b\n"
   if $OPTIONS->{backuphistory} && !$OPTIONS->{backup};

push @errors, "ERROR: Can't use flag -m without also specifying -n\n"
   if defined($OPTIONS->{nicName}) && !$OPTIONS->{nic};

push @errors, "ERROR: Incorrect number of arguments\n"
   if @ARGV;

usage(@errors) if @errors;

Note that the above fixes numerous errors in your code.


Help vs Usage Error

  • --help should provide the requested help to STDOUT, and shouldn't result in an error exit code.
  • Usage errors should be printed to STDERR, and should result in an error exit code.

Calling HelpMessage indifferently in both situations is therefore incorrect.

Create the following sub named usage to use (without arguments) when GetOptions returns false, and with an error message when some other usage error occurs:

use File::Basename qw( basename );

sub usage {
   my $prog = basename($0);
   print STDERR $_ for @_;
   print STDERR "Try '$prog --help' for more information.\n";
   exit(1);
}

Keep using HelpMessage in response to --help, but the defaults for the arguments are not appropriate for --help. You should use the following:

'help' => sub { HelpMessage( -exitval => 0, -verbose => 1 ) },
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • I wondered if it would be impossible because of multiple conditions being true, but based on other answers, it seems that it is possible to still build a table and compare... – Speeddymon Nov 15 '17 at 16:50
  • What are you talking about? No answer used a dispatch table. All the answers (including mine) used a (`for` or `map`) loop that performs as many checks as there are conditions. The points of a dispatch table is to do a single check no matter how many conditions there are. Since all conditions can be true, you need to check all conditions, so a dispatch table is impossible by definition. (And that's without even mentioning that the value of a dispatch table should be a code reference or similar (something to dispatch to).) – ikegami Nov 15 '17 at 16:57
  • The difference between mine and the others is that mine avoids using an inefficient unordered hash and uses an efficient ordered list instead. (You could place the list in an array if you prefer.) – ikegami Nov 15 '17 at 16:58
  • Updated to match updated question. That fact that none of the other answers can be extended for your updated question proves my pointthat trying to put everything into one loop or table just makes things less flexible, longer and more complex. – ikegami Nov 15 '17 at 17:16
  • In response to the "help" tip -- ```HelpMessage``` is defined by ```GetOpt::Long``` and reads from the PODs at the end of the file. – Speeddymon Nov 15 '17 at 18:34
  • Adjusted the last section of my answer accordingly. – ikegami Nov 15 '17 at 18:47
0

You can use a dispatch table if there are a lot of options. I would build that table programmatically. It might not be the best option here, but it works and the configuration is more readable than your elsif construct.

use strict;
use warnings;
use Ref::Util::XS 'is_arrayref';    # or Ref::Util

sub create_key {
    my $input = shift;

    # this would come from somewhere else, probably the Getopt config
    my @opts = qw( host bash nic nicName console virtual cmdb
        policyid showcompliant backup backuphistory page );

    # this is to cover the configuration with easier syntax
    $input = { map { $_ => 1 } @{$input} }
        if is_arrayref($input);

    # options are always prefilled with false values
    return join q{}, map { $input->{$_} ? 1 : 0 }
        sort @opts;
}

my %forbidden_combinations = (
    map { create_key( $_->[0] ) => $_->[1] } (
        [ [qw( console virtual )] => q{Cannot use flags -r and -v together} ],
        [ [qw( console cmdb )]    => q{Cannot use flags -r and -d together} ],
        [ [qw( console backup )]  => q{Cannot use flags -r and -b together} ],
        [ [qw( console nic )]     => q{Cannot use flags -r and -n together} ],
    )
);

p %forbidden_combinations; # from Data::Printer

The output of the p function is the dispatch table.

{
    00101   "Cannot use flags -r and -v together",
    00110   "Cannot use flags -r and -n together",
    01100   "Cannot use flags -r and -d together",
    10100   "Cannot use flags -r and -b together"
}

As you can see, we've sorted all the options ascii-betically to use them as keys. That way, you could in theory build all kinds of combinations like exclusive options.

Let's take a look at the configuration itself.

my %forbidden_combinations = (
    map { create_key( $_->[0] ) => $_->[1] } (
        [ [qw( console virtual )] => q{Cannot use flags -r and -v together} ],
        # ...
    )
);

We use a list of array references. Each entry is on one line and contains two pieces of information. Using the fat comma => makes it easy to read. The first part, which is much like a key in a hash, is the combination. It's a list of fields that should not occur together. The second element in the array ref is the error message. I've removed all the recurring elements, like the newline, to make it easier to change how and where the error can be displayed.

The map around this list of combination configuration runs the options through our create_key function, which translates it to a simple bitmap-style string. We assign all of it to a hash of that map and the error message.

Inside create_key, we check if it was called with an array reference as its argument. If that's the case, the call was for building the table, and we convert it to a hash reference so we have a proper map to look stuff up in. We know that the %OPTIONS always contains all the keys that exist, and that those are pre-filled with values that all evaluate to false. We can harness that convert the truthiness of those values to 1 or 0, which then builds our key.

We will see in a moment why that is useful.

Now how do we use this?

sub HelpMessage { exit; }; # as a placeholder

# set up OPTIONS
my %OPTIONS = (
    host          => q{},
    bash          => 0,
    nic           => 0,
    nicName       => q{},
    console       => 0,
    virtual       => 0,
    cmdb          => 0,
    policyid      => 0,
    showcompliant => 0,
    backup        => 0,
    backuphistory => 0,
    page          => q{},
);

# read options with Getopt::Long ...
$OPTIONS{console} = $OPTIONS{virtual} = 1;

# ... and check for wrong invocations
if ( exists $forbidden_combinations{ my $key = create_key($OPTIONS) } ) {
    warn "ERROR: $forbidden_combinations{$key}\n";
    HelpMessage;
}

All we need to do now is get the $OPTIONS hash reference from Getopt::Long, and pass it through our create_key function to turn it into the map string. Then we can simply see if that key exists in our %forbidden_combinations dispatch table and show the corresponding error message.


Advantages of this approach

If you want to add more parameters, all you need to do is include them in @opts. In a full implementation that would probably be auto-generated from the config for the Getopt call. The keys will change under the hood, but since that is abstracted away you don't have to care.

Furthermore, this is easy to read. The create_key aside, the actual dispatch table syntax is quite concise and even has documentary character.

Disadvantages of this approach

There is a lot of programmatic generation going on for just a single call. It's certainly not the most efficient way to do it.


To take this further, you can write functions that auto-generate entries for certain scenarios.

I suggest you take a look at the second chapter in Mark Jason Dominus' excellent book Higher-Order Perl, which is available for free as a PDF.

simbabque
  • 53,749
  • 8
  • 73
  • 136
  • Thank you for the detailed answer. I've updated the question to help clarify how the ```$OPTIONS``` hash is setup. Can your example work within the bounds of what I have already, or should I rewrite the whole thing from scratch? – Speeddymon Nov 15 '17 at 16:44
  • @Speeddymon yeah, that should work. I see you've got `%OPTIONS`, and it is always pre-set with values. That's going to be interesting. Let me try. – simbabque Nov 15 '17 at 16:47
  • Speaking of the HOP book... That was actually what I was using to try to learn and where I was having trouble in applying the concept to my code. :-) I couldn't find a PDF version before, so thank you for the link! – Speeddymon Nov 15 '17 at 17:08
  • 1
    @Speeddymon I've updated the answer and changed it to match your updated code. I suggest you read the diff first. What I don't like about it yet is that the possible keys are there twice, but that can be solved with some more trickery. I think that would blow up the answer even more, so I didn't do that. – simbabque Nov 15 '17 at 17:17
  • Doesn't detect the case when `-r`, `-v` and `-b` are provided as an error. – ikegami Nov 16 '17 at 06:33
  • @ikegami huh, true. Why didn't I think of that? – simbabque Nov 16 '17 at 07:36