IB2017 has asked for the wisdom of the Perl Monks concerning the following question:

I need your expertise in this apparently small problem

Given the following hash (installed TTS voices available on Windows 10):

%voices = { 'Microsoft Hedda Desktop - German' => 3, 'Microsoft Haruka Desktop - Japanese' => 4, 'Microsoft Zira Desktop - English (United States)' => 0, 'Microsoft Hazel Desktop - English (Great Britain)' => 1, 'Microsoft David Desktop - English (United States)' => 2, 'Microsoft Huihui Desktop - Chinese (Simplified)' => 5 };

I need to match a voice by its language. If there are more voices for one language, I just need to pick one. I do this as follows:

use List::Util qw<first>; my $language='English'; my $voice = $voices{ ( first { m/$language/ } keys %voices ) || '' };

So far so good. It pickis the first voice, in the above 0. Now the problem: for misterious Windows reasons, if there are more English Voices, the voice 0 can get in conflict with other voices, with the effect of Windows using the wrong one. My temporary and probably idea is to "jump" 0 if there are other English voices. If hashes would be sortable, I would simply sort the has by its value (from big to small).

%voices = { 'Microsoft Huihui Desktop - Chinese (Simplified)' => 5 'Microsoft Haruka Desktop - Japanese' => 4, 'Microsoft Hedda Desktop - German' => 3, 'Microsoft David Desktop - English (United States)' => 2, 'Microsoft Hazel Desktop - English (Great Britain)' => 1, 'Microsoft Zira Desktop - English (United States)' => 0, };

With a hash ordered like this, if there are more than 2 English voices, "0" will never be picked up.

Any compact idea I am unable to think of?

Replies are listed 'Best First'.
Re: Alternative to sort Hash
by soonix (Chancellor) on Dec 06, 2019 at 10:47 UTC
    It looks like SpeechSynthesizer.GetInstalledVoices returns something like an array, and you converted that to a hash pointing to array indexes. Why not simply use the array, and let List::Util's first give you the index directly? For your problem with index 0, replace the first array element with something unmatchable, and if first returns undef, it's the case where you have to use 0 anyway.
    my @voices = ...(whatever you do to get the voices array) my $voicezero = $voices[0]; # if name of voice needed later $voices[0] = 'least-liked'; my $voice = first { m/$language/ } @voices || 0; $voices[0] = $voicezero; # if name of voice needed later

      It could be even simpler than that. Assuming operations on an array, if one can get the array into a function and decide on a regex to match the desired language:

      sub func { ... return unless my @wanted = grep m{ $rx_wanted }xms, @voices; return $wanted[ -1 ]; }
      func returns undef if no desired language is found, the last language if more than one is found, and the first if there is only one. If one didn't want the last language in the case of more than one, maybe something like
          return $wanted[ @wamted > 1 ? 1 : 0 ];


      Give a man a fish:  <%-{-{-{-<

Re: Alternative to sort Hash
by kcott (Archbishop) on Dec 06, 2019 at 12:04 UTC

    G'day IB2017,

    The first code statement you presented has problems. You would have seen this had you used strict and warnings. Your code basically has this form:

    $ perl -e '%x = { a => 1, b => 2 }' $

    With warnings you would have found this problem:

    $ perl -e 'use warnings; %x = { a => 1, b => 2 }' Name "main::x" used only once: possible typo at -e line 1. Reference found where even-sized list expected at -e line 1. $

    With strict you would have found this problem:

    $ perl -e 'use strict; %x = { a => 1, b => 2 }' Global symbol "%x" requires explicit package name (did you forget to d +eclare "my %x"?) at -e line 1. Execution of -e aborted due to compilation errors. $

    All your Perl scripts should start with:

    use strict; use warnings;

    Your next problem seems to be that you think hashes are ordered collections: they are not. If you look at keys, values or each, you'll find the same information:

    "Hash entries are returned in an apparently random order. ..."

    Read any of those for more information about this.

    Using the empty string as a key into %voices will result in an uninitialised value. That's another potential problem; although, you don't show any code that deals with that.

    Here's a general solution:

    #!/usr/bin/env perl use strict; use warnings; my %all_voices = ( 'Microsoft Hedda Desktop - German' => 3, 'Microsoft Haruka Desktop - Japanese' => 4, 'Microsoft Zira Desktop - English (United States)' => 0, 'Microsoft Hazel Desktop - English (Great Britain)' => 1, 'Microsoft David Desktop - English (United States)' => 2, 'Microsoft Huihui Desktop - Chinese (Simplified)' => 5 ); for my $language (qw{German Japanese French English Chinese}) { my @available_voices = grep /^[^-]+-\s+$language/, keys %all_voice +s; if (@available_voices) { my $first_voice = ( sort { $all_voices{$a} <=> $all_voices{$b} } @available_voices )[0]; printf "%8s : [%d] %s\n", $language, $all_voices{$first_voice}, $first_voice; } else { printf "%8s : No voice found\n", $language; } }

    Output:

    German : [3] Microsoft Hedda Desktop - German Japanese : [4] Microsoft Haruka Desktop - Japanese French : No voice found English : [0] Microsoft Zira Desktop - English (United States) Chinese : [5] Microsoft Huihui Desktop - Chinese (Simplified)

    To remove the English voice with value 0, when there are more than 1 English voices, you can simply add a filter like this:

    ... if (@available_voices) { if (@available_voices > 1) { @available_voices = grep $all_voices{$_}, @available_voice +s; } ...

    Now output is this:

    German : [3] Microsoft Hedda Desktop - German Japanese : [4] Microsoft Haruka Desktop - Japanese French : No voice found English : [1] Microsoft Hazel Desktop - English (Great Britain) Chinese : [5] Microsoft Huihui Desktop - Chinese (Simplified)

    Finally, you seem (as far as I can tell) to be asking for a "compact" solution. Less code is not necessarily better code: it's often less readable and therefore hard to maintain and potentially error-prone; and, after Perl's optimisations, there's no guarantee it's any more efficient. What exactly are you requirements for this code? Why do you think a "compact" solution would be preferable?

    — Ken

      Less code is not necessarily better code
      True, but OP's %voices looks like it's generated from something like {map {$_ => $n++} @list_of_voices} which (in my eyes) is unnecessary, especially as he uses List::Util, already.
Re: Alternative to sort Hash
by hippo (Archbishop) on Dec 06, 2019 at 09:55 UTC
    Any compact idea I am unable to think of?

    Just remove the 0-valued entry from the hash before you start? That's a simple way not to choose it.

    It isn't entirely clear to me from your description precisely what it is you want to achieve, unfortunately. Perhaps a test would help? ie: How to ask better questions using Test::More and sample data

      I cannot remove "0". If on the Windows machine there is only one English voice installed, I need to pick up this (and it will be 0). I simply need to pick up any English voice which is not 0 if other English voices are there.

        So this, then?

        use strict; use warnings; use Test::More tests => 4; my $voices = { 'Microsoft Hedda Desktop - German' => 3, 'Microsoft Haruka Desktop - Japanese' => 4, 'Microsoft Zira Desktop - English (United States)' => 0, 'Microsoft Hazel Desktop - English (Great Britain)' => 1, 'Microsoft David Desktop - English (United States)' => 2, 'Microsoft Huihui Desktop - Chinese (Simplified)' => 5 }; my $engtest = bestvoice ('English', $voices); cmp_ok $voices->{$engtest}, '>', 0, 'English has positive value'; like $engtest, qr/English/, 'English matched in key'; is bestvoice ('German', $voices), 'Microsoft Hedda Desktop - German', 'German matched in key'; is bestvoice ('Esperanto', $voices), undef, 'Esperanto not matched'; sub bestvoice { my ($want, $v) = @_; my @poss = grep {/$want/} keys %$v; if ($#poss > 0) { @poss = grep { $v->{$_} } @poss; } return $poss[0]; }
        So do that exactly? sub pickVoice... No hash thought-jam but simple api