0

I'm trying to create a time converter based on some sample cities and their respective time zones. I'm retrieving the current time in UTC, then adding or subtracting according to the offset from UTC for each timezone, then I'm adding or subtracting 12 to convert the time to its corresponding 12-hour format so it could be either am or pm. Then this information is showed on a TimePicker when the user selects a city from a spinner.

Now the problem is that I'm getting the correct hour, but for some time zones the am\pm is backwards. So for example, my local time is EST, and I want to convert to PST. Let's say it's 7pm, and I want to know the time in LA. Instead of showing 4pm, it shows 4am.

So I'm having trouble "correcting" the times. I used .HOUR_OF_DAY which I think is supposed to account for Daylight Saving, and I tried using HOUR but that wouldn't solve the problem and would just set the time back one hour. The corrective math with the twelve hours is needed to convert the 24-hour format to 12 -hour, but this doesn't work as I intended because, as I mentioned, while it does set to the correct hour, it doesn't account for the right am/pm according to the actual time. Also, .setIs24HourView is set to false.

Anyway, here's the function that takes care of this functionality:

public int convertTime(String city)
    {
        //Result of taking in the UTC time and adding/subtracting the offset
        int offset = 0;

        //gets the calender instance of time with GMT standard, then getting hour of day
        Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        int UTC = c.get(Calendar.HOUR_OF_DAY);

        //set offset according to city
        switch(city)
        {
            case "New York":
                offset = UTC-4;
                break;
            case "London":
                offset = UTC+1;
                break;
            case "Los Angeles":
                offset = UTC-7;
                break;
            case "Dubai":
                offset= UTC+4;
                break;
            case "Paris":
                offset = UTC+2;
                break;
            case "Moscow":
                offset = UTC+3;
                break;
            case "Cairo":
                offset = UTC+2;
                break;
            case "Hong Kong":
                offset = UTC+8;
                break;
            case "Beijing":
                offset = UTC+8;
                break;
            case "New Delhi":
                offset= UTC+5;
                break;
            case "Mexico City":
                offset = UTC-5;
                break;
            case "Brasilia":
                offset = UTC-3;
                break;
        }

        //if the offset is in the AM
        if(offset < 12)
        {
            //set am
            offset = offset+12;
        }
        //if the offset is in the PM
        else if(offset > 12)
        {
            //set pm
            offset = offset-12;
        }
        else
           //its twelve o'clock
            offset = 12;

        return offset;
    }

And here's how it would appear in the app, for visualization: Time Converter

EDIT: Sorry, I should have added this as well. So the offset is returning the "conversion factor", which I used in the onItemSelected event for the spinner. So when the user selects an item from the spinner, this function reads the entry, and sets the time according to the offset value (that is, the hour, and minute too, but that's statically set since it will always return the correct minute):

 @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
 {
    if(convertSpinner.getSelectedItemPosition() == 0)
        {
            //display current/local time
            int hour = c.get(Calendar.HOUR_OF_DAY);
            convertTime.setHour(hour);
            //currentTime.setHour(hour);
        }
        else if(convertSpinner.getSelectedItemPosition()== 1)
            convertTime.setHour(conversionFactory("New York"));
        else if(convertSpinner.getSelectedItemPosition()== 2)
            convertTime.setHour(conversionFactory("London"));
        else if(convertSpinner.getSelectedItemPosition()== 3)
            convertTime.setHour(conversionFactory("Los Angeles"));
        //... same processs for the other cities, shortened for obvious reasons
        else 
            convertTime.setHour(12);
        //set the minute
        int minute = c.get(Calendar.MINUTE);
        convertTime.setMinute(minute);
  }

Also here's my main:

private TimePicker currentTime, convertTime;
private Spinner convertSpinner;

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
    {
        View v= inflater.inflate(R.layout.fragment_time, container, false);
        convertTime = v.findViewById(R.id.convert_clock);
        convertSpinner = v.findViewById(R.id.convert_spinner);

        convertTime.setIs24HourView(false);
        convertTime.setClickable(false);

        //for convert spinner
        ArrayAdapter<CharSequence> adapter2 = ArrayAdapter.createFromResource(getActivity(),R.array.time_cities, android.R.layout.simple_spinner_item);
        adapter2.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        convertSpinner.setAdapter(adapter2);
        convertSpinner.setOnItemSelectedListener(this);

        return v;
}
Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
Akelah
  • 3
  • 2
  • There’s something missing here. I don’t see anything in your code actually setting AM or PM. In the case where it’s 4 PM in LA, what does your method return, and what should it return instead? [How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example). – Ole V.V. Dec 28 '20 at 18:44
  • 1
    (1) You should definitely seriously consider throwing out the `Calendar` class and using [java.time, the modern Java date and time API,](https://docs.oracle.com/javase/tutorial/datetime/) instead. Yes, you can use it on Android. (2) Most of the places in your list use summer time (DST), so don’t have the same offset all year round. Leave the conversion to `ZonedDateTime` and `ZoneId` from java.time rather than trying to do it yourself. – Ole V.V. Dec 28 '20 at 18:48

2 Answers2

1

You don't need to reinvent the wheel.

Instead of putting so much of error-prone complex logic, you should use the OOTB (Out-Of-The-Box) date-time API. Both, the modern as well the legacy API, have much easier ways to do what you are trying to do with your complex custom logic.

However, the legacy date-time API i.e. the ones from java.util and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern date-time API. For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Defining a timezone requires not just the City but also the Continent.

The standard format to represent a timezone string is Continent/City.

Using the modern API:

import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String... args) {
        // Test
        System.out.println(getOffset("Europe/London"));
        System.out.println(getOffset("Africa/Johannesburg"));
        System.out.println(getOffset("America/Chicago"));
        System.out.println(getOffset("Asia/Calcutta"));
    }

    public static ZoneOffset getOffset(String zoneId) {
        return ZonedDateTime.now(ZoneId.of(zoneId)).getOffset();
    }
}

Output:

Z
+02:00
-06:00
+05:30

Note that Z stands for Zulu time which specifies UTC (which has timezone offset of +00:00 hours).

Using legacy API:

import java.util.TimeZone;

public class Main {
    public static void main(String... args) {
        // Test
        System.out.println(getOffset("Europe/London"));
        System.out.println(getOffset("Africa/Johannesburg"));
        System.out.println(getOffset("America/Chicago"));
        System.out.println(getOffset("Asia/Calcutta"));
    }

    public static String getOffset(String zoneId) {
        int offset = TimeZone.getTimeZone(zoneId).getRawOffset();// milliseconds to be added to UTC
        int seconds = offset / 1000;
        int hours = seconds / 3600;
        int minutes = (seconds % 3600) / 60;
        return "GMT" + (offset >= 0 ? "+" : "-") + String.format("%02d", Math.abs(hours)) + ":"
                + String.format("%02d", minutes);
    }
}

Output:

GMT+00:00
GMT+02:00
GMT-06:00
GMT+05:30

How to convert a date-time from one timezone to the other:

With the modern date-time API, you can use ZonedDateTime#withZoneSameInstant and OffsetDateTime#withOffsetSameInstant to get the ZonedDateTime and OffsetDateTime respectively in the specified timezone.

Demo of ZonedDateTime

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String... args) {
        // Test
        ZonedDateTime zdt = ZonedDateTime.now(ZoneId.systemDefault());
        System.out.println(zdt);
        System.out.println(getZonedDateTimeWithZoneId(zdt, "Europe/London"));
        System.out.println(getZonedDateTimeWithZoneId(zdt, "Africa/Johannesburg"));
        System.out.println(getZonedDateTimeWithZoneId(zdt, "America/Chicago"));
        System.out.println(getZonedDateTimeWithZoneId(zdt, "Asia/Calcutta"));
    }

    public static ZonedDateTime getZonedDateTimeWithZoneId(ZonedDateTime zdt, String zoneId) {
        return zdt.withZoneSameInstant(ZoneId.of(zoneId));
    }
}

Output:

2020-12-28T20:03:54.093476Z[Europe/London]
2020-12-28T20:03:54.093476Z[Europe/London]
2020-12-28T22:03:54.093476+02:00[Africa/Johannesburg]
2020-12-28T14:03:54.093476-06:00[America/Chicago]
2020-12-29T01:33:54.093476+05:30[Asia/Calcutta]

Demo of OffsetDateTime

import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;

public class Main {
    public static void main(String... args) {
        // Test
        OffsetDateTime odt = OffsetDateTime.now(ZoneId.systemDefault());
        System.out.println(odt);
        System.out.println(getOffsetDateTimeWithZoneId(odt, "Europe/London"));
        System.out.println(getOffsetDateTimeWithZoneId(odt, "Africa/Johannesburg"));
        System.out.println(getOffsetDateTimeWithZoneId(odt, "America/Chicago"));
        System.out.println(getOffsetDateTimeWithZoneId(odt, "Asia/Calcutta"));
    }

    public static OffsetDateTime getOffsetDateTimeWithZoneId(OffsetDateTime odt, String zoneId) {
        return odt.withOffsetSameInstant(getOffset(zoneId));
    }

    public static ZoneOffset getOffset(String zoneId) {
        return ZonedDateTime.now(ZoneId.of(zoneId)).getOffset();
    }
}

Output:

2020-12-28T20:08:25.026349Z
2020-12-28T20:08:25.026349Z
2020-12-28T22:08:25.026349+02:00
2020-12-28T14:08:25.026349-06:00
2020-12-29T01:38:25.026349+05:30

Using the legacy API:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Main {
    public static void main(String... args) throws ParseException {
        // Test
        Date date = Calendar.getInstance().getTime();
        System.out.println(date);
        System.out.println(getFormattedDateTimeWithZoneId(date, "Europe/London"));
        System.out.println(getFormattedTimeWithZoneId(date, "Europe/London"));
        System.out.println(getFormattedDateTimeWithZoneId(date, "Africa/Johannesburg"));
        System.out.println(getFormattedTimeWithZoneId(date, "Africa/Johannesburg"));
        System.out.println(getFormattedDateTimeWithZoneId(date, "America/Chicago"));
        System.out.println(getFormattedTimeWithZoneId(date, "America/Chicago"));
        System.out.println(getFormattedDateTimeWithZoneId(date, "Asia/Calcutta"));
        System.out.println(getFormattedTimeWithZoneId(date, "Asia/Calcutta"));
    }

    public static String getFormattedDateTimeWithZoneId(Date date, String zoneId) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss a zzz", Locale.ENGLISH);
        sdf.setTimeZone(TimeZone.getTimeZone(getOffset(zoneId)));
        return sdf.format(date);
    }

    public static String getFormattedTimeWithZoneId(Date date, String zoneId) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss a", Locale.ENGLISH);
        sdf.setTimeZone(TimeZone.getTimeZone(getOffset(zoneId)));
        return sdf.format(date);
    }

    public static String getOffset(String zoneId) {
        int offset = TimeZone.getTimeZone(zoneId).getRawOffset();// milliseconds to be added to UTC
        int seconds = offset / 1000;
        int hours = seconds / 3600;
        int minutes = (seconds % 3600) / 60;
        return "GMT" + (offset >= 0 ? "+" : "-") + String.format("%02d", Math.abs(hours)) + ":"
                + String.format("%02d", minutes);
    }
}

Output:

Mon Dec 28 20:47:25 GMT 2020
2020-12-28 08:47:25 PM GMT+00:00
08:47:25 PM
2020-12-28 10:47:25 PM GMT+02:00
10:47:25 PM
2020-12-28 02:47:25 PM GMT-06:00
02:47:25 PM
2020-12-29 02:17:25 AM GMT+05:30
02:17:25 AM

Learn about the modern date-time API from Trail: Date Time.

Arvind Kumar Avinash
  • 71,965
  • 6
  • 74
  • 110
0

java.time

Consider using java.time, the modern Java date and time API, for your time work. In particular when it’s non-trivial as yours is.

To get the current time in LA:

    LocalTime timeInLa = LocalTime.now(ZoneId.of("America/Los_Angeles"));
    System.out.println("Current time in LA: " + timeInLa);

Output when I ran just now:

Current time in LA: 11:10:18.975

For other cities supply the corresponfing time zone ID, for example Europe/London or Asia/Dubai. The ID for Brasilia is America/Sao_Paulo. I suggest that you build a map from city name to time zone ID so you can get the ID in a matter of a map lookup and won’t need the long switch statement.

Once we’ve got a LocalTime object, we can take out the hour of day.

    int hourOfDayInLa = timeInLa.getHour();
    System.out.println("Hour of day in LA: " + hourOfDayInLa);

Hour of day in LA: 11

Whehter you need to make any adjustments to this value, and which, to have your time picker display the time right — I would certainly not expect so, but it’s not something I know nor can tell you.

What I can tell you is that we can also get the clock hour of AM or PM and either 0 for AM or 1 for PM from the LocalTime object:

    int clockHourOfAmOrPm = timeInLa.get(ChronoField.CLOCK_HOUR_OF_AMPM);
    int amOrPmIndexInLa = timeInLa.get(ChronoField.AMPM_OF_DAY);
    String amPm = amOrPmIndexInLa == 0 ? "AM" : "PM";
    System.out.format("Clock hour %d (1 through 12) %s (code %d)%n",
            clockHourOfAmOrPm, amPm, amOrPmIndexInLa);

Clock hour 11 (1 through 12) AM (code 0)

(If the end goal was a string AM or PM, we should use a formatter for that: I was thinking that if you wanted to do further processing based on whether it was AM or PM, an int value of 0 or 1 would be more practical for that.)

Question: Doesn’t java.time require Android API level 26?

java.time works nicely on both older and newer Android devices. It just requires at least Java 6.

  • In Java 8 and later and on newer Android devices (from API level 26) the modern API comes built-in.
  • In non-Android Java 6 and 7 get the ThreeTen Backport, the backport of the modern classes (ThreeTen for JSR 310; see the links at the bottom).
  • On older Android either use desugaring or the Android edition of ThreeTen Backport. It’s called ThreeTenABP. In the latter case make sure you import the date and time classes from org.threeten.bp with subpackages.

Links

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161