diff --git a/lib/DDG/Goodie/ConvertLatLon.pm b/lib/DDG/Goodie/ConvertLatLon.pm index 8fb47bbfd..1406a9bfd 100644 --- a/lib/DDG/Goodie/ConvertLatLon.pm +++ b/lib/DDG/Goodie/ConvertLatLon.pm @@ -13,6 +13,7 @@ zci is_cached => 1; name 'Convert Latitude and Longitude'; description 'Convert between latitudes and longitudes expressed in degrees of arc and decimal'; primary_example_queries '71º 10\' 3" in decimal'; +secondary_example_queries '71 degrees 10 minutes 3 seconds east in decimal', '- 16º 30\' 0" - 68º 9\' 0" as decimal'; category 'transformations'; topics 'geography', 'math', 'science'; code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/ConvertLatLon.pm'; @@ -82,112 +83,140 @@ my %cardinalName = ( handle query_nowhitespace => sub { - return unless $_ =~ /$latLonQR/; + return unless /$latLonQR/; - my $minus = $+{minus}; - my $degrees = $+{degrees}; - my $minutes = $+{minutes}; - my $seconds = $+{seconds}; - my $cardinal = $+{cardinal}; + my @queries; + my @results; - #Validation: must have minutes if has seconds - return unless (($minutes && $seconds) || ! $seconds); + my $toFormat; - #Validation: can't supply both minus sign and cardinal direction - return if $minus && $cardinal; + #Loop over all provided latitudes/longitudes + # Not going to try and enforce strict latitude/longitude + # pairing — if the user wants to pass in a long list + # of latitudes or a single longitude, no problem as long + # as they're all in the same format + while (/$latLonQR/g) { - #Convert cardinal to standardised name if provided - $cardinal = $cardinalName{lc($cardinal)} if $cardinal; + my $minus = $+{minus}; + my $degrees = $+{degrees}; + my $minutes = $+{minutes}; + my $seconds = $+{seconds}; + my $cardinal = $+{cardinal}; - #Set the sign - my $sign; - if ($cardinal) { - $sign = $cardinalSign{$cardinal}; - } else { - $sign = $minus ? -1 : 1; - } + #Validation: must have minutes if has seconds + return unless (($minutes && $seconds) || ! $seconds); - #Determine type of conversion (dms -> decimal or decimal -> dms) - # and perform as appropriate + #Validation: can't supply both minus sign and cardinal direction + return if $minus && $cardinal; - #If the degrees are expressed in decimal... - if ($degrees =~ /\./) { + #Convert cardinal to standardised name if provided + $cardinal = $cardinalName{lc($cardinal)} if $cardinal; - #Validation: must not have provided minutes and seconds - return if $minutes || $seconds; - - #Validation: if only degrees were provided, make sure - # the user isn't looking for a temperature or trigonometric conversion - my $rejectQR = qr/temperature|farenheit|celcius|radians/; - return if $_ =~ /$rejectQR/i; - - #Validation: can't exceed 90 degrees (if latitude) or 180 degrees - # (if longitude or unknown) - if ($cardinal && $cardinal =~ /[NS]/) { - return if abs($degrees) > 90; + #Set the sign + my $sign; + if ($cardinal) { + $sign = $cardinalSign{$cardinal}; } else { - return if abs($degrees) > 180; + $sign = $minus ? -1 : 1; } - #Convert - (my $dmsDegrees, my $dmsMinutes, my $dmsSeconds, my $dmsSign) = decimal2dms($degrees * $sign); + #Determine type of conversion (dms -> decimal or decimal -> dms) + # and perform as appropriate - #Annoyingly, Geo::Coordinates::DecimalDegrees will sign the degrees as - # well as providing a sign - $dmsDegrees = abs($dmsDegrees); + #If the degrees are expressed in decimal... + if ($degrees =~ /\./) { - #If seconds is fractional, take the mantissa - $dmsSeconds = round($dmsSeconds); + #If this isn't the first conversion, make sure the + # user hasn't passed a mix of decimal and DMS + return if $toFormat && ! $toFormat eq 'DMS'; + $toFormat = 'DMS'; - #Format nicely - my $formattedDMS = format_dms($dmsDegrees, $dmsMinutes, $dmsSeconds, $dmsSign, $cardinal); - my $formattedQuery = format_decimal(($degrees * $sign)); + #Validation: must not have provided minutes and seconds + return if $minutes || $seconds; - return $formattedDMS, html => wrap_html($formattedQuery, $formattedDMS, 'DMS'); + #Validation: if only degrees were provided, make sure + # the user isn't looking for a temperature or trigonometric conversion + my $rejectQR = qr/temperature|farenheit|celcius|radians/; + return if $_ =~ /$rejectQR/i; - #Otherwise, we assume type is DMS (even if no - # minutes/seconds given) - } else { + #Validation: can't exceed 90 degrees (if latitude) or 180 degrees + # (if longitude or unknown) + if ($cardinal && $cardinal =~ /[NS]/) { + return if abs($degrees) > 90; + } else { + return if abs($degrees) > 180; + } - #Validation: must have given at least minutes - return unless $minutes; + #Convert + (my $dmsDegrees, my $dmsMinutes, my $dmsSeconds, my $dmsSign) = decimal2dms($degrees * $sign); - #Validation: can't have decimal minutes if there are seconds - return if $minutes =~ /\./ && $seconds; + #Annoyingly, Geo::Coordinates::DecimalDegrees will sign the degrees as + # well as providing a sign + $dmsDegrees = abs($dmsDegrees); - #Validation: minutes and seconds can't exceed 60 - return if $minutes >= 60; - return if $seconds && $seconds >= 60; + #If seconds is fractional, take the mantissa + $dmsSeconds = round($dmsSeconds); - #Apply the sign - $degrees = $sign * $degrees; + #Format nicely + my $formattedDMS = format_dms($dmsDegrees, $dmsMinutes, $dmsSeconds, $dmsSign, $cardinal); + my $formattedQuery = format_decimal(($degrees * $sign)); - #Convert - # Note that unlike decimal2dms, dms2decimal requires a signed degrees - # and returns a signed degrees (not a separate sign variable) - my $decDegrees = $seconds ? dms2decimal($degrees, $minutes, $seconds) : dm2decimal($degrees, $minutes); + push(@queries, $formattedQuery); + push(@results, $formattedDMS); - #Round to 8 significant figures - $decDegrees = FormatSigFigs($decDegrees, 8); - $decDegrees =~ s/\.$//g; - - #Validation: can't exceed 90 degrees (if latitude) or 180 degrees - # (if longitude or unknown) - if ($cardinal && $cardinal =~ /[NS]/) { - return if abs($decDegrees) > 90; + #Otherwise, we assume type is DMS (even if no + # minutes/seconds given) } else { - return if abs($decDegrees) > 180; + + #If this isn't the first conversion, make sure the + # user hasn't passed a mix of decimal and DMS + return if $toFormat && ! $toFormat eq 'decimal'; + $toFormat = 'decimal'; + + #Validation: must have given at least minutes + return unless $minutes; + + #Validation: can't have decimal minutes if there are seconds + return if $minutes =~ /\./ && $seconds; + + #Validation: minutes and seconds can't exceed 60 + return if $minutes >= 60; + return if $seconds && $seconds >= 60; + + #Apply the sign + $degrees = $sign * $degrees; + + #Convert + # Note that unlike decimal2dms, dms2decimal requires a signed degrees + # and returns a signed degrees (not a separate sign variable) + my $decDegrees = $seconds ? dms2decimal($degrees, $minutes, $seconds) : dm2decimal($degrees, $minutes); + + #Round to 8 significant figures + $decDegrees = FormatSigFigs($decDegrees, 8); + $decDegrees =~ s/\.$//g; + + #Validation: can't exceed 90 degrees (if latitude) or 180 degrees + # (if longitude or unknown) + if ($cardinal && $cardinal =~ /[NS]/) { + return if abs($decDegrees) > 90; + } else { + return if abs($decDegrees) > 180; + } + + #Format nicely + my $formattedDec = format_decimal($decDegrees); + my $formattedQuery = format_dms($degrees, $minutes, $seconds, $sign, $cardinal); + + push(@queries, $formattedQuery); + push(@results, $formattedDec); + } - - #Format nicely - my $formattedDec = format_decimal($decDegrees); - my $formattedQuery = format_dms($degrees, $minutes, $seconds, $sign, $cardinal); - - return $formattedDec, html => wrap_html($formattedQuery, $formattedDec, 'decimal'); - } - return; + my $answer = join(' ' , @results); + my $html = wrap_html(\@queries, \@results, $toFormat); + + return $answer, html => $html; }; #Format a degrees-minutes-seconds expression @@ -221,6 +250,7 @@ sub format_decimal { #Add a minus sign if negative (decimal format # never uses cardial notation) + # TODO this is incorrect change this if ($decDegrees / abs($decDegrees) == -1) { $formatted = '−' . $formatted; } @@ -237,11 +267,23 @@ sub append_css { return "$html"; } +sub wrap_secondary { + my $secondary = shift; + return "" . $secondary . ""; +} + sub wrap_html { - my ($query, $result, $toFormat) = @_; - my $from = encode_entities($query) . " " . " in " . encode_entities($toFormat) . ":". ""; - my $to = encode_entities($result); - return append_css("
$from $to
"); + + #TODO can we use prototypes in this version of perl? + my @queries = @{$_[0]}; + my @results = @{$_[1]}; + my $toFormat = $_[2]; + + my $queries = join wrap_secondary(', '), map { encode_entities($_) } @queries; + my $results = join wrap_secondary(', '), map { encode_entities($_) } @results; + + my $html = "
" . $queries . wrap_secondary(' in ' . $toFormat . ': ') . $results . "
"; + return append_css($html); } 1; diff --git a/t/ConvertLatLon.t b/t/ConvertLatLon.t index 4394bebdf..ab2f4662f 100644 --- a/t/ConvertLatLon.t +++ b/t/ConvertLatLon.t @@ -19,22 +19,18 @@ ddg_goodie_test( #Secondary examples '71 degrees 10 minutes 3 seconds east in decimal' => test_zci('71.1675°', html => qr/71\.1675/), + '- 16º 30\' 0" - 68º 9\' 0" as decimal' => test_zci('−16.5° −68.15°', html => qr/−16\.5.+−68\.15/), #Latitudes and longitudes of cities, various trigger combinations #Values from Wikipedia/GeoHack toolserver #Sydney - 'convert 151.2094° E to degrees minutes seconds' => test_zci('151° 12′ 34″ E', html => qr/151.+12.+34.+E/), + 'convert 33.859972º S 151.2094° E to degrees minutes seconds' => test_zci('33° 51′ 36″ S 151° 12′ 34″ E', html => qr/33.+51.+36.+S.+151.+12.+34.+E/), #Moscow - '55° 45′ 00″ in decimal' => test_zci('55.75°', html => qr/55.75/), + '55° 45′ 0″ 37° 37′ 0″ in decimal' => test_zci('55.75° 37.616667°', html => qr/55\.75.+37\.616667/), #Kinshasha - 'kinshasha is 15.322222 degrees east convert to dms' => test_zci('15° 19′ 20″ E', html => qr/15.+19.+20.+E/), + 'kinshasha is 4.325 degrees south 15.322222 degrees east convert to dms' => test_zci('4° 19′ 30″ S 15° 19′ 20″ E', html => qr/4.+19.+30.+S.+15.+19.+20.+E/), #Copenhagen '55.676111° latitude' => test_zci('55° 40′ 34″', html => qr/55.+40.+34/), - #La Paz - '- 68º 9\' 0" as decimal' => test_zci('−68.15°', html => qr/−68.15/), - - #If two coordinates are given (e.g. lat/lon), only the first will be converted - '20 º 10 \' 0 " S, 57 deg 31 min 0 sec E convert' => test_zci('−20.166667°', html => qr/−20\.166667/), #Make sure "plural S" works "68 degrees 9 minutes S in decimal form" => test_zci('−68.15°', html => qr/−68.15/), @@ -59,6 +55,10 @@ ddg_goodie_test( #Check for css '71° 10\' 3" in decimal' => test_zci(qr/./, html => qr/css/), + #Check for to-format name + '16.5° S, 68.15° W dms' => test_zci(qr/./, html => qr/DMS/), + '16° 30′ S, 68° 9′ W decimal' => test_zci(qr/./, html => qr/decimal/), + ); done_testing;