Multiple threads use the same SimpleDateFormat without ThreadLocal - java

I am trying to understand the need of using ThreadLocal. Lot of people mention ThreadLocal should be used to provide a Per-Thread SimpleDateFormat, but they do not mention how will a mangled SimpleDateFormat will be seen if ThreadLocal is not used. I try the following code, seems it is just fine, I don't see a mangled SimpleDateFormat.
import java.text.SimpleDateFormat;
import java.util.Date;
public class ThreadLocalTest {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
private static final Date TODAY = new Date();
private static final String expected = "07/09/2016";
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
#Override
public void run() {
for (int j = 0; j < 1000; j++) {
String real = dateFormat.format(TODAY);
if (!real.equals(expected)) {
throw new RuntimeException("Mangled SimpleDateFormat");
}
}
}
}).start();
}
}
}
How can I produce a exception like NumberFormatException because I don't use a ThreadLocal ?

The crucial point is: the SimpleDateFormat implementation is not thread safe.
That doesn't mean that it will throw an exception.
It is worse: maybe, occasionally the shared formatter will simply give you wrong output!
You know, if "multi-threading issues" would nicely throw exceptions at you ... people would be much less afraid of them. Because we would have a direct hint that something went wrong.
Instead, things go wrong - and unnoticed.
Suggestion: enhance your test to
always format the same Date object
check that the result of formatting that Date is as expected (for example by comparing it against the result of an initial, first formatting operation)
And of course: only print mismatches, so that notice when they happen. Or better: throw your own exception on mismatch!
EDIT: turns out that the "better" way to enforce inconsistencies is to not use formatting but parsing!
Finally, to address another comment: of course, inconsistencies can only occur for objects that are shared between multiple threads. When each thread has its own format object, than there is no sharing; thus no problem.

Just run these code, you will get "java.lang.NumberFormatException". If not occur, run a few more times
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class ThreadLocalDemo1 implements Runnable {
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ThreadLocalDemo1 td = new ThreadLocalDemo1();
Thread t1 = new Thread(td, "Thread-1");
Thread t2 = new Thread(td, "Thread-2");
t1.start();
t2.start();
}
#Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread run execution started for " + Thread.currentThread().getName());
System.out.println("Date formatter pattern is " + simpleDateFormat.toPattern());
try {
System.out.println("Formatted date is " + simpleDateFormat.parse("2013-05-24 06:02:20"));
} catch (ParseException pe) {
pe.printStackTrace();
}
System.out.println("=========================================================");
}
}
}

Date formats are not thread safe.
I think if you format the same day you can't reproduce, you should use 2 different dates or format also second and dates that have different seconds etc. Date format uses a Calendar under the hood on which it sets the date. If the first thread sets a date and start formatting the string and another thread with a different date comes and sets it on the same calendar, you will get wrong output.
Following code produces an exception/error:
final Date today = new Date();
String expectedToday = dateFormat.format(today);
Date yesterday = new Date(today.getTime() - TimeUnit.DAYS.toMillis(1));
String expectedYesterday = dateFormat.format(yesterday);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
#Override
public void run() {
while (true) {
String format = dateFormat.format(today);
if (!expectedToday.equals(format)) {
System.out.println("error: " + format + " " + expectedToday);//Throw exception if you want
}
format = dateFormat.format(yesterday);
if (!expectedYesterday.equals(format)) {
System.out.println("error: " + format + " " + expectedYesterday);//Throw exception if you want
}
}
}
}).start();
}

One of the ways that SimpleDateFormat is not thread safe is that it has an internal calendar field, which holds a Calendar object. Pretty much the first thing that SimpleDateFormat does before actually formatting the date is call this.calendar.setTime(theDateYouPassedIn), no synchronization or locks. I'm not sure if this is the only way, but it should be fairly straightforward to inspect the code.
So, one way to get SimpleDateFormat to fail is to use dates that will produce differing output in different threads. Here is an example:
public class NotThreadSafe
{
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
public static void main(String[] args) {
Date dref = new Date();
// Dates for yesterday and tomorrow
Date[] ds = new Date[] {
new Date(dref.getTime() - (24L * 60L * 60L * 1000L)),
new Date(dref.getTime() + (24L * 60L * 60L * 1000L))
};
String[] refs = new String[ds.length];
for (int i = 0; i < ds.length; ++i) {
// How the corresponding ds[i] should be formatted
refs[i] = dateFormat.format(ds[i]);
}
for (int i = 0; i < 100; i++) {
// Every even numbered thread uses ds[0] and refs[0],
// odd numbered threads use ds[1] and refs[1].
int index = (i % 2);
final Date d = ds[index];
final String ref = refs[index];
new Thread(new Runnable() {
#Override
public void run() {
while (true) {
String s = dateFormat.format(d);
if (!ref.equals(s)) {
throw new IllegalStateException("Expected: " + ref + ", got: " + s);
}
}
}
}).start();
}
}
}
As the comments show, every even numbered thread will format yesterday's date, and odd numbered threads will use tomorrows date.
If you run this, threads will pretty much immediately start committing suicide by throwing exceptions until such time as you have only a handful left, likely all formatting the same date.

Related

How to make real-time updates of an Excel .csv file in Java when time comparison evaluates as true?

I'll start by saying I'm open to other suggestions if there's an easier way to do this. My goal is to update a CSV file with stock data at 9 am and 4 pm EST (not my time zone) daily. I feel I am very close. What I have now includes a getEST function that pulls the current time in the EST timezone (properly accounting for Daylight Savings) and returns a boolean value of true/false if that time matches the opening or closing time I hardwired. I also have the program creating a CSV file and appending to it each time. I need to make some modifications to my code so that it automatically executes my data retrieval code when these boolean flags output as true.
I have it set up right now to utilize a reset counter telling it to wait either 7 or 17 hours depending on the value of that counter. So my question is a) Would it be better to push the reset when the EST clock reaches midnight? and b) How do I start the program at any time so that it outputs at the right time (i.e. if the app launches at 6 EST it waits 3 hours before pulling the data?
I'll snippet the code a bit since I have a lot. Let me know if any missing portion would be helpful. I'm aware my encapsulation can be improved as well, just want to get it working first and then change more variables to private after.
public class Scheduler {
//Declare common variables
static int reset = 0;
public static void main(String[] args) {
//Manual implementation for testing purposes
DJIA obj = new DJIA();
EST tu = new EST();
obj.getDJIA();
tu.getEST();
try {
obj.createCSV();
} catch (IOException e) {
e.printStackTrace();
}
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
Runnable runnable = new Runnable() {
public void run()
{
//Moving implementation code here causes program to wait 7 hours before executing
}
};
// Execute program at 9 am EST and 4 pm EST
if(reset == 0) {
service.schedule(runnable, 7, TimeUnit.HOURS);
reset ++;
}
else {
service.schedule(runnable, 17, TimeUnit.HOURS);
reset = 0;
}
}
}
public class EST {
//Make time strings usable by main class
public String est;
public String o_time;
public String c_time;
public boolean marketOpen;
public boolean marketClose;
//Gets timezone in EST
public void getEST() {
SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("EST5EDT"));
est = dateFormat.format(new Date());
checkTime();
//System.out.println("Current time (EST): " + est);
//System.out.println("Is it 9 am EST? " + marketOpen);
}
//Check if the stock market is open or not, returning a boolean indicator
public void checkTime() {
o_time = "09:00:00";
c_time = "16:00:00";
if(est.equals(o_time)) {
marketOpen = true;
}
else {
marketOpen = false;
}
if(est.equals(c_time)) {
marketClose = true;
}
else {
marketClose = false;
}
}
}
//Snipet from DJIA class
public void makeCSV() throws IOException {
//Create output file only if it does not already exist
File f = new File("DJIA.csv");
path = Paths.get("C:\\Users\\zrr81\\eclipse-workspace\\SelTest\\DJIA.csv");
if(!f.exists()) {
try {
//Create FileWriter object with file as parameter
String fid = "DJIA.csv";
CSVWriter writer = new CSVWriter(new FileWriter(fid, true));
String[] headers = "Stock, Opening Price, Closing Price".split(",");
writer.writeNext(headers);
writer.close();
}catch (Exception ex) {
ex.printStackTrace();
}
}
else {
}
}
public void createCSV() throws IOException {
//Insert if else logic to import to proper array
makeCSV();
o_values.add(stock);
o_values.add("test");
c_values.add("#Empty");
c_values.add("test");
I was able to solve this by building a new function that calculates the time duration between the current time (in EST) and the next market open/close time using the modern Java.time utility:
public class TimeUntil extends EST {
//Create commonly used EST function
EST est = new EST();
public static LocalTime d1;
public static LocalTime d2;
public static LocalTime d3;
public static long diff;
public int wait = 0;
//Main time calculator
public void calcTime() {
est.getEST();
try {
d1 = LocalTime.parse(est.est);
d2 = LocalTime.parse(est.o_time);
d3 = LocalTime.parse(est.c_time);
Duration duration = Duration.between(d1, d2);
diff = duration.toHours();
if(diff > 0 || diff < -7) {
tillOpen();
}
else {
tillClose();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
public int tillOpen(){
if(diff > 0) {
wait = (int) diff;
return wait;
}
else {
int x = Math.abs((int) diff);
wait = 24 - x;
return wait;
}
}
public int tillClose() {
wait = Math.abs((int) diff);
return wait;
}
}
This accepts negative values (i.e. how long it's been since market open) and translates them into a positive time if it's past market close. By subtracting from 24, it returns the correct value for time until the market opens again and successfully launches at the proper time

Best method for parsing date formats during import datas

I created method for parsing a view different date formats during data import (400 K records). My method catches ParseException and trying to parse date with next format when it's different.
Question: Is better way(and faster) to set correct date format during data import?
private static final String DMY_DASH_FORMAT = "dd-MM-yyyy";
private static final String DMY_DOT_FORMAT = "dd.MM.yyyy";
private static final String YMD_DASH_FORMAT = "yyyy-MM-dd";
private static final String YMD_DOT_FORMAT = "yyyy.MM.dd";
private static final String SIMPLE_YEAR_FORMAT = "yyyy";
private final List<String> dateFormats = Arrays.asList(YMD_DASH_FORMAT, DMY_DASH_FORMAT,
DMY_DOT_FORMAT, YMD_DOT_FORMAT);
private Date parseDateFromString(String date) throws ParseException {
if (date.equals("0")) {
return null;
}
if (date.length() == 4) {
SimpleDateFormat simpleDF = new SimpleDateFormat(SIMPLE_YEAR_FORMAT);
simpleDF.setLenient(false);
return new Date(simpleDF.parse(date).getTime());
}
for (String format : dateFormats) {
SimpleDateFormat simpleDF = new SimpleDateFormat(format);
try {
return new Date(simpleDF.parse(date).getTime());
} catch (ParseException exception) {
}
}
throw new ParseException("Unknown date format", 0);
}
If you're running single threaded, an obvious improvement is to create the SimpleDateFormat objects only once. In a multithreaded situation using ThreadLocal<SimpleDateFormat> would be required.
Also fix your exception handling. It looks like it's written by someone who shouldn't be trusted to import any data.
For a similar problem statememt , i had used time4j library in the past. Here is an example. This uses the following dependencies given below as well
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.time4j.PlainDate;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.MultiFormatParser;
import net.time4j.format.expert.ParseLog;
import net.time4j.format.expert.PatternType;
public class MultiDateParser {
static final MultiFormatParser<PlainDate> MULTI_FORMAT_PARSER;
static {
ChronoFormatter<PlainDate> style1 = ChronoFormatter.ofDatePattern("dd-MM-yyyy", PatternType.CLDR,
Locale.GERMAN);
ChronoFormatter<PlainDate> style2 = ChronoFormatter.ofDatePattern("dd.MM.yyyy", PatternType.CLDR, Locale.US);
ChronoFormatter<PlainDate> style3 = ChronoFormatter.ofDatePattern("yyyy-MM-dd", PatternType.CLDR, Locale.US);
ChronoFormatter<PlainDate> style4 = ChronoFormatter.ofDatePattern("yyyy.MM.dd", PatternType.CLDR, Locale.US);
//this is not supported
//ChronoFormatter<PlainDate> style5 = ChronoFormatter.ofDatePattern("yyyy", PatternType.CLDR, Locale.US);
MULTI_FORMAT_PARSER = MultiFormatParser.of(style1, style2, style3, style4);
}
public List<PlainDate> parse() throws ParseException {
String[] input = { "11-09-2001", "09.11.2001", "2011-11-01", "2011.11.01", "2012" };
List<PlainDate> dates = new ArrayList<>();
ParseLog plog = new ParseLog();
for (String s : input) {
plog.reset(); // initialization
PlainDate date = MULTI_FORMAT_PARSER.parse(s, plog);
if (date == null || plog.isError()) {
System.out.println("Wrong entry found: " + s + " at position " + dates.size() + ", error-message="
+ plog.getErrorMessage());
} else {
dates.add(date);
}
}
System.out.println(dates);
return dates;
}
public static void main(String[] args) throws ParseException {
MultiDateParser mdp = new MultiDateParser();
mdp.parse();
}
}
<dependency>
<groupId>net.time4j</groupId>
<artifactId>time4j-core</artifactId>
<version>4.19</version>
</dependency>
<dependency>
<groupId>net.time4j</groupId>
<artifactId>time4j-misc</artifactId>
<version>4.19</version>
</dependency>
The case yyyy will have to be handled differently as it is not a date. May be similar logic that you have used (length ==4) is a choice.
The above code returns , you can check a quick perf run to see if this scales for the 400k records you have.
Wrong entry found: 2012 at position 4, error-message=Not matched by any format: 2012
[2001-09-11, 2001-11-09, 2011-11-01, 2011-11-01]
Talking about 400K records, it might be reasonable to do some "bare hands" optimization here.
For example: if your incoming string has a "-" on position 5, then you know that the only (potentially) matching format would be "yyyy-MM-dd". If it is "."; you know that it is the other format that starts yyyy.
So, if you really want to optimize, you could fetch that character and see what it is. Could save 3 attempts of parsing with the wrong format!
Beyond that: I am not sure if sure if "dd" means that your other dates start with "01" ... or if "1.1.2016" would be possible, too. If all your dates always use two digits for dd/mm; then you can repeat that game - as you would fetch on position 3 - to choose between "dd...." and "dd-....".
Of course; there is one disadvantage - if you follow that idea, you are very much "hard-coding" the expected formats into your code; so adding other formats will become harder. On the other hand; you would save a lot.
Finally: the other thing that might greatly speed up things would be to use stream operations for reading/parsing that information; because then you could look into parallel streams, and simply exploit the ability of modern hardware to process 4, 8, 16 dates in parallel.

How can I know if the user is inserting a valid Date or not in Java's Swing?

I am creating a project in which I am supposed to take date of birth of any person. So for that I have taken 3 combo boxes: date, month and year. But how I will know that the Date which is going to be inserted is valid because the number of day is different-different months and years is different.
And is there any ready made GUI component for taking dates from users?
I am designing using Swing package.
My sample code is
import java.awt.*;
import javax.swing.*;
public class Pro1 extends JFrame
{
JComboBox dd, mm, yy;
JLabel doblbl;
Container con;
public Pro1()
{
con = getContentPane();
doblbl = new JLabel("Date of Birth :-");
doblbl.setHorizontalAlignment(SwingConstants.CENTER);
doblbl.setFont(new Font("Arial",Font.BOLD,17));
doblbl.setForeground(Color.blue);
doblbl.setOpaque(true);
doblbl.setBackground(new Color(230,180,230));
dd = new JComboBox();
mm = new JComboBox();
yy = new JComboBox();
for(int i = 1; i<=31; i++)
dd.addItem("" + i);
for(int i = 1; i<=12; i++)
mm.addItem("" + i);
for(int i = 1960; i<=2014; i++)
yy.addItem("" + i);
con.setLayout(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
int i = 120;
doblbl.setBounds(30,i+=40,270,30);
int j = 120;
dd.setBounds(350,j+=40,50,30);
mm.setBounds(420,j,50,30);
yy.setBounds(480,j,70,30);
con.add(doblbl);
con.add(dd);
con.add(mm);
con.add(yy);
setSize(1500,800);
setVisible(true);
con.setBackground(new Color(125,80,140));
}
public static void main(String s[])
{
Pro1 p1 = new Pro1();
}
}
You can use SimpleDateFormat to parse or format the date as dd/MM/yyyy.
Sample code:
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
String dateString = "30/02/2014"; // form date string in above format
String formattedDate = sdf.format(sdf.parse(dateString));
if (formattedDate.equals(dateString)) {
System.out.println("valid date");
} else {
System.out.println("Invalid date"); // It's an invalid date
}
Consider offering the user a JSpinner with a SpinnerDateModel, then it is easier for the user to enter, or scroll through and select, a valid date.
import java.awt.*;
import java.util.*;
import javax.swing.*;
public class PromptForDate {
public static void main(String[] args) {
Runnable r = new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception useDefault) {}
SpinnerDateModel dateModel = new SpinnerDateModel(
new Date(), null, null, Calendar.DAY_OF_MONTH );
JSpinner dateSpinner = new JSpinner(dateModel);
dateSpinner.setEditor(
new JSpinner.DateEditor(dateSpinner, "dd/MM/yyyy"));
JOptionPane.showMessageDialog(
null, dateSpinner, "D.O.B.?",
JOptionPane.QUESTION_MESSAGE);
System.out.println(dateModel.getValue());
}
};
SwingUtilities.invokeLater(r);
}
}
And is there any ready made GUI component for taking dates from users?
Yes, there are several options in standard API and third-party libraries, as exposed in this answer: JSpinner + SpinnerDateModel or JCalendar or JXDatePicker. These components will also solve this other problem:
But how i will know that the date which is going to be inserted is
valid because the number of dates in different-different months and
years is different.
Off-topic
About this:
con.setLayout(null);
...
dd.setBounds(350,j+=40,50,30);
mm.setBounds(420,j,50,30);
yy.setBounds(480,j,70,30);
...
con.add(dd);
con.add(mm);
con.add(yy);
While using Null layout is perfectly possible, Swing is designed to be used along with layout managers and thus null layout is discouraged. See this topic Why is it frowned upon to use a null layout in SWING?.

Counter Invoked in Two ActionListeners

I have a counter x that I want to invoke in two separate ActionListeners. When I try to make x into final, I can't increment using x++;. I tried to make x within the nest, but then I can't use the same value in the other ActionListener. Code is as follows:
buttonIn.addActionListener(new ActionListener() {
String reportDate = "";
int x = 0;
public void actionPerformed(ActionEvent e)
{
DateFormat df = new SimpleDateFormat("MM/dd/yyyy hh:mm aa");
Date time = new GregorianCalendar().getTime();
reportDate = df.format(time);
String confirm = name.getText() + " has checked in at " + reportDate;
timeLabel.setText(confirm);
timeLabel.setVisible(true);
String action = "Time In";
reportData[x][0] = name.getText();
reportData[x][1] = "Time In";
reportData[x][2] = reportDate;
x++;
System.out.println(x);
}
});
buttonOut.addActionListener(new ActionListener() {
String reportDate = "";
public void actionPerformed(ActionEvent e)
{
DateFormat df = new SimpleDateFormat("MM/dd/yyyy hh:mm aa");
Date time = new GregorianCalendar().getTime();
reportDate = df.format(time);
String confirm = name.getText() + " has checked out at " + reportDate;
timeLabel.setText(confirm);
timeLabel.setVisible(true);
reportData[x][0] = name.getText();
reportData[x][1] = "Time Out";
reportData[x][2] = reportDate;
x++;
}
});
One simple option is to use AtomicInteger instead - then the variable can be final, but you can still increment the wrapped value. So:
final AtomicInteger counter = new AtomicInteger(0);
buttonIn.addActionListener(new ActionListener() {
// Within here, you can use counter.get and counter.incrementAndGet
});
buttonOut.addActionListener(new ActionListener() {
// Within here, you can use counter.get and counter.incrementAndGet
});
I'd also strongly consider extracting that code into a separate class though - almost all the code is the same, so you should be able to remove the duplication by parameterizing the differences. So you'd end up with something like:
AtomicInteger counter = new AtomicInteger(0);
buttonIn.addActionListener(new ReportListener(
counter, reportData, "%s has checked in at %s", "Time In"));
buttonOut.addActionListener(new ReportListener(
counter, reportData, "%s has checked out at %s", "Time Out"));
(Where ReportListener is the new class implementing ActionListener.)
Additionally:
I strongly suspect you want to use HH rather than hh in your SimpleDateFormat
Consider which time zone and locale you want to use in your SimpleDateFormat, and specify them explicitly for clarity
To get the current time, just call new Date() rather than creating a calendar and extracting the date from it
There's no obvious reason for having reportDate as an instance variable
For testability, I'd encourage you to use some sort of Clock interface, with an implementation provided by dependency injection, so you can fake time appropriately
Consider using Joda Time for all date/time work; it's much cleaner than the built-in date/time API

TimeZone customized display name

I need a timezone display values as follows :
(UTC + 05:30) Chennai, Kolkata, Mumbai, New Delhi
But by using following method I am getting bit different output. How should I get the timezone display name as above ? (if required, I can use JODA).
public class TimeZoneUtil {
private static final String TIMEZONE_ID_PREFIXES =
"^(Africa|America|Asia|Atlantic|Australia|Europe|Indian|Pacific)/.*";
private static List<TimeZone> timeZones;
public static List<TimeZone> getTimeZones() {
if (timeZones == null) {
timeZones = new ArrayList<TimeZone>();
final String[] timeZoneIds = TimeZone.getAvailableIDs();
for (final String id : timeZoneIds) {
if (id.matches(TIMEZONE_ID_PREFIXES)) {
timeZones.add(TimeZone.getTimeZone(id));
}
}
Collections.sort(timeZones, new Comparator<TimeZone>() {
public int compare(final TimeZone t1, final TimeZone t2) {
return t1.getID().compareTo(t2.getID());
}
});
}
return timeZones;
}
public static String getName(TimeZone timeZone) {
return timeZone.getID().replaceAll("_", " ") + " - " + timeZone.getDisplayName();
}
public static void main(String[] args) {
timeZones = getTimeZones();
for (TimeZone timeZone : timeZones) {
System.out.println(getName(timeZone));
}
}
}
This code may do the trick for you:
public static void main(String[] args) {
for (String timeZoneId: TimeZone.getAvailableIDs()) {
TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
// Filter out timezone IDs such as "GMT+3"; more thorough filtering is required though
if (!timeZoneId.matches(".*/.*")) {
continue;
}
String region = timeZoneId.replaceAll(".*/", "").replaceAll("_", " ");
int hours = Math.abs(timeZone.getRawOffset()) / 3600000;
int minutes = Math.abs(timeZone.getRawOffset() / 60000) % 60;
String sign = timeZone.getRawOffset() >= 0 ? "+" : "-";
String timeZonePretty = String.format("(UTC %s %02d:%02d) %s", sign, hours, minutes, region);
System.out.println(timeZonePretty);
}
}
The output looks like this:
(UTC + 09:00) Tokyo
There are, however, a few caveats:
I only filter out timezones whose ID matches the format "continent/region" (e.g. "America/New_York"). You would have to do a more thorough filtering process to get rid of outputs such as (UTC - 08:00) GMT+8 though.
You should read the documentation for TimeZone.getRawOffSet() and understand what it's doing. For example, it doesn't DST effects into consideration.
On the whole, you should know that this is a messy approach, primarily because the timezone ID can be of so many different formats. Maybe you could restrict yourself down to the timezones that matter for your application, and just have a key value mapping of timezone IDs to display names?

Categories