Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plural quantities #1607

Merged
merged 2 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ android {

buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""

minSdk 21
minSdk 24
targetSdk 34

testInstrumentationRunner "de.dennisguse.opentracks.TestRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.junit.Assert.assertEquals;

import android.content.Context;
import android.icu.text.MessageFormat;
import android.util.Pair;

import androidx.test.core.app.ApplicationProvider;
Expand All @@ -15,6 +16,7 @@

import java.time.Duration;
import java.util.Locale;
import java.util.Map;

import de.dennisguse.opentracks.LocaleRule;
import de.dennisguse.opentracks.content.data.TestDataUtil;
Expand Down Expand Up @@ -191,6 +193,34 @@ public void getAnnouncement_imperial_speed() {
assertEquals("Total distance 12.4 miles. 1 hour 5 minutes 10 seconds. Average moving speed 11.4 miles per hour.", announcement);
}

@Test
public void getAnnouncement_imperial_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.ofMile(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));

// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL_FEET, true, null, null).toString();

// then
assertEquals("Total distance 1.1 miles. 1 hour. Average moving speed 1.1 miles per hour.", announcement);
}

@Test
public void getAnnouncement_metric_speed_1() {
TrackStatistics stats = new TrackStatistics();
stats.setTotalDistance(Distance.ofKilometer(1.1));
stats.setTotalTime(Duration.ofHours(2).plusMinutes(5).plusSeconds(10));
stats.setMovingTime(Duration.ofHours(1));

// when
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();

// then
assertEquals("Total distance 1.1 kilometers. 1 hour. Average moving speed 1.1 kilometers per hour.", announcement);
}

@Test
public void getAnnouncement_withInterval_imperial_speed() {
// given
Expand Down Expand Up @@ -306,4 +336,19 @@ public void getAnnouncement_only_lap_heart_rate() {
// then
assertEquals(" Current heart rate 133 bpm.", announcement);
}

@Test
public void ICUMessageDemo() {
// Android 7's ICU MessageFormat; working
String template = """
{n, plural,
one {1 mile}
other {{n,number,#.#} miles}
}""";

assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.1)));
assertEquals("1 mile", MessageFormat.format(template, Map.of("n", 1)));
assertEquals("1.1 miles", MessageFormat.format(template, Map.of("n", 1.11)));
assertEquals("1.2 miles", MessageFormat.format(template, Map.of("n", 1.18)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static de.dennisguse.opentracks.settings.PreferencesUtils.shouldVoiceAnnounceTotalDistance;

import android.content.Context;
import android.icu.text.MessageFormat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.TtsSpan;
Expand All @@ -17,6 +18,7 @@
import androidx.annotation.Nullable;

import java.time.Duration;
import java.util.Map;

import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance;
Expand Down Expand Up @@ -45,22 +47,22 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic
switch (unitSystem) {
case METRIC -> {
perUnitStringId = R.string.voice_per_kilometer;
distanceId = R.plurals.voiceDistanceKilometers;
speedId = R.plurals.voiceSpeedKilometersPerHour;
distanceId = R.string.voiceDistanceKilometersPlural;
speedId = R.string.voiceSpeedKilometersPerHourPlural;
unitDistanceTTS = "kilometer";
unitSpeedTTS = "kilometer per hour";
}
case IMPERIAL_FEET -> {
perUnitStringId = R.string.voice_per_mile;
distanceId = R.plurals.voiceDistanceMiles;
speedId = R.plurals.voiceSpeedMilesPerHour;
distanceId = R.string.voiceDistanceMilesPlural;
speedId = R.string.voiceSpeedMilesPerHourPlural;
unitDistanceTTS = "mile";
unitSpeedTTS = "mile per hour";
}
case NAUTICAL_IMPERIAL -> {
perUnitStringId = R.string.voice_per_nautical_mile;
distanceId = R.plurals.voiceDistanceNauticalMiles;
speedId = R.plurals.voiceSpeedMKnots;
distanceId = R.string.voiceDistanceNauticalMilesPlural;
speedId = R.string.voiceSpeedMKnotsPlural;
unitDistanceTTS = "nautical mile";
unitSpeedTTS = "knots";
}
Expand All @@ -73,7 +75,8 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic
builder.append(context.getString(R.string.total_distance));
// Units should always be english singular for TTS.
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceInUnit, 1, unitDistanceTTS);
String template = context.getResources().getString(distanceId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", distanceInUnit)), distanceInUnit, 1, unitDistanceTTS);
// Punctuation helps introduce natural pauses in TTS
builder.append(".");
}
Expand All @@ -93,15 +96,17 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic
double speedInUnit = averageMovingSpeed.to(unitSystem);
builder.append(" ")
.append(context.getString(R.string.speed));
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedInUnit, 1, unitSpeedTTS);
String template = context.getResources().getString(speedId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", speedInUnit)), speedInUnit, 1, unitSpeedTTS);
builder.append(".");
}
if (shouldVoiceAnnounceLapSpeedPace() && currentDistancePerTime != null) {
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
if (currentDistancePerTimeInUnit > 0) {
builder.append(" ")
.append(context.getString(R.string.lap_speed));
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
String template = context.getResources().getString(speedId);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", currentDistancePerTimeInUnit)), currentDistancePerTimeInUnit, 1, unitSpeedTTS);
builder.append(".");
}
}
Expand Down Expand Up @@ -147,23 +152,22 @@ static Spannable getAnnouncement(Context context, TrackStatistics trackStatistic
return builder;
}

static int getQuantityCount(double d) {
return (int) d;
}

private static void appendDuration(@NonNull Context context, @NonNull SpannableStringBuilder builder, @NonNull Duration duration) {
int hours = (int) (duration.toHours());
int minutes = (int) (duration.toMinutes() % 60);
int seconds = (int) (duration.getSeconds() % 60);

if (hours > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceHours, hours, hours), hours, 0, "hour");
String template = context.getResources().getString(R.string.voiceHoursPlural);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", hours)), hours, 0, "hour");
}
if (minutes > 0) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceMinutes, minutes, minutes), minutes, 0, "minute");
String template = context.getResources().getString(R.string.voiceMinutesPlural);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", minutes)), minutes, 0, "minute");
}
if (seconds > 0 || duration.isZero()) {
appendDecimalUnit(builder, context.getResources().getQuantityString(R.plurals.voiceSeconds, seconds, seconds), seconds, 0, "second");
String template = context.getResources().getString(R.string.voiceSecondsPlural);
appendDecimalUnit(builder, MessageFormat.format(template, Map.of("n", seconds)), seconds, 0, "second");
}
}

Expand Down
90 changes: 72 additions & 18 deletions src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -554,29 +554,65 @@ limitations under the License.
<string name="value_smallest_recommended">Smallest (recommended)</string>
<!-- Voice -->
<plurals name="voiceHours">
<item quantity="one">1 hour</item>
<item quantity="other">%1$d hours</item>
<item quantity="one">1 hour [Replaced by VoiceHoursPlural]</item>
<item quantity="other">%1$d hours [Replaced by VoiceHoursPlural]</item>
</plurals>
<plurals name="voiceMinutes">
<item quantity="one">1 minute</item>
<item quantity="other">%1$d minutes</item>
<item quantity="one">1 minute [Replaced by VoiceMinutesPlural]</item>
<item quantity="other">%1$d minutes [Replaced by VoiceMinutesPlural]</item>
</plurals>
<plurals name="voiceSeconds">
<item quantity="one">1 second</item>
<item quantity="other">%1$d seconds</item>
<item quantity="one">1 second [Replaced by voiceSecondsPlural]</item>
<item quantity="other">%1$d seconds [Replaced by voiceSecondsPlural]</item>
</plurals>
<plurals name="voiceSpeedKilometersPerHour">
<item quantity="one">1 kilometer per hour</item>
<item quantity="other">%1$.1f kilometers per hour</item>
<item quantity="one">1 kilometer per hour [Replaced by voiceSpeedKilometersPerHourPlural]</item>
<item quantity="other">%1$.1f kilometers per hour [Replaced by voiceSpeedKilometersPerHourPlural]</item>
</plurals>
<plurals name="voiceSpeedMilesPerHour">
<item quantity="one">1 mile per hour</item>
<item quantity="other">%1$.1f miles per hour</item>
<item quantity="one">1 mile per hour [Replaced by voiceSpeedMilesPerHourPlural]</item>
<item quantity="other">%1$.1f miles per hour [Replaced by voiceSpeedMilesPerHourPlural]</item>
</plurals>
<plurals name="voiceSpeedMKnots">
<item quantity="one">1 knot</item>
<item quantity="other">%1$.1f knots</item>
<item quantity="one">1 knot [Replaced by voiceSpeedMKnotsPlural]</item>
<item quantity="other">%1$.1f knots [Replaced by voiceSpeedMKnotsPlural]</item>
</plurals>
<string name="voiceHoursPlural">
{n, plural,
=1 {1 hour}
other {{n,number} hours}
}
</string>
<string name="voiceMinutesPlural">
{n, plural,
=1 {1 minute}
other {{n,number} minutes}
}
</string>
<string name="voiceSecondsPlural">
{n, plural,
=1 {1 second}
other {{n,number} seconds}
}
</string>
<string name="voiceSpeedKilometersPerHourPlural">
{n, plural,
=1 {1 kilometer per hour}
other {{n,number,#.0} kilometers per hour}
}
</string>
<string name="voiceSpeedMilesPerHourPlural">
{n, plural,
=1 {1 mile per hour}
other {{n,number,#.0} miles per hour}
}
</string>
<string name="voiceSpeedMKnotsPlural">
{n, plural,
=1 {1 knot}
other {{n,number,#.0} knots}
}
</string>
<string name="voice_on_device_speaker_title">Use the device\'s speaker</string>
<string name="voice_per_kilometer">per kilometer</string>
<string name="voice_per_mile">per mile</string>
Expand All @@ -587,17 +623,35 @@ limitations under the License.
<string name="speed">Average moving speed</string>
<string name="total_distance">Total distance</string>
<plurals name="voiceDistanceKilometers">
<item quantity="one">1 kilometer</item>
<item quantity="other">%1$.1f kilometers</item>
<item quantity="one">1 kilometer [Replaced by voiceDistanceKilometersPlural]</item>
<item quantity="other">%1$.1f kilometers [Replaced by voiceDistanceKilometersPlural]</item>
</plurals>
<plurals name="voiceDistanceMiles">
<item quantity="one">1 mile</item>
<item quantity="other">%1$.1f miles</item>
<item quantity="one">1 mile [Replaced by voiceDistanceMilesPlural]</item>
<item quantity="other">%1$.1f miles [Replaced by voiceDistanceMilesPlural]</item>
</plurals>
<plurals name="voiceDistanceNauticalMiles">
<item quantity="one">1 nautical mile</item>
<item quantity="other">%1$.1f nautical miles</item>
<item quantity="one">1 nautical mile [Replaced by voiceDistanceNauticalMilesPlural]</item>
<item quantity="other">%1$.1f nautical miles [Replaced by voiceDistanceNauticalMilesPlural]</item>
</plurals>
<string name="voiceDistanceKilometersPlural">
{n, plural,
=1 {1 kilometer}
other {{n,number,#.0} kilometers}
}
</string>
<string name="voiceDistanceMilesPlural">
{n, plural,
=1 {1 mile}
other {{n,number,#.0} miles}
}
</string>
<string name="voiceDistanceNauticalMilesPlural">
{n, plural,
=1 {1 nautical mile}
other {{n,number,#.0} nautical miles}
}
</string>
<string name="average_heart_rate">Average heart rate</string>
<string name="current_heart_rate">Current heart rate</string>
<!-- Waypoint Type -->
Expand Down