tags:

views:

502

answers:

16

This is a nasty one for me... I'm a PHP guy working in Java on a JSP project. I know how to do what I'm attempting through too much code and a complete lack of finesse. I'd prefer to do it RIGHT. :) Here is the situation:

I'm writing a small display to show customers what days they can water their lawns based on their watering group (ABCDE) and what time of year it is. Our seasons look like this: Summer (5-1 to 8-31) Spring (3-1 to 4-30) Fall (9-1 to 10-31) Winter (11-1 to 2-28)

An example might be:

If I'm in group A, here would be my allowed times: Winter: Mondays only Spring: Tues, Thurs, Sat Summer: Any Day Fall: Tues, Thurs, Sat

If I was writing this in PHP I would use arrays like this:

//M=Monday,t=Tuesday,T=Thursday.... etc
$schedule["A"]["Winter"]='M';
$schedule["A"]["Spring"]='tTS';
$schedule["A"]["Summer"]='Any';
$schedule["A"]["Fall"]='tTS';
$schedule["B"]["Winter"]='t';

I COULD make the days arrays (array("Tuesday","Thursday","Saturday")) etc, but it is not necessary for what I'm really trying to accomplish.

I will also need to setup arrays to determine what season I'm in:

$seasons["Summer"]["start"]=0501;
$seasons["Summer"]["end"]=0801;

Can anyone suggest a really cool way to do this? I will have today's date and the group letter. I will need to get out of my function a day (M) or a series of days (tTS), (Any).

Thanks, folks!

+3  A: 

You could do essentially the same code with Hashtables (or some other Map):

Hashtable<String, Hashtable<String, String>> schedule
    = new Hashtable<String, Hashtable<String, String>>();
schedule.put("A", new Hashtable<String, String>());
schedule.put("B", new Hashtable<String, String>());
schedule.put("C", new Hashtable<String, String>());
schedule.put("D", new Hashtable<String, String>());
schedule.put("E", new Hashtable<String, String>());

schedule.get("A").put("Winter", "M");
schedule.get("A").put("Spring", "tTS");
// Etc...

Not as elegant, but then again, Java isn't a dynamic language, and it doesn't have hashes on the language level.

Note: You might be able to do a better solution, this just popped in my head as I read your question.

Mike Stone
Use a HashMap instead of a Hashtable for better performance. Hashtable, like Vector, is REALLY old and should not be used for performance reasons. Mainly, because everything's synchronized, when it shouldn't be.
MetroidFan2002
A: 
import java.util.Hashtable;

public static void main(string[] args)
{
  Hashtable seasons = new Hashtable();
  Hashtable summer = new Hashtable();
  summer.put("start", *MUST_WRAP_PRIMITIVES*);
  summer.put("end", *MUST_WRAP_PRIMITIVES*);
  seasons.put("summer", summer);
}

I do believe that should serve you well. Hashtables are what PHP calls "associative arrays" (like what you typed up above). The MUST_WRAP_PRIMITIVES lines mean what they say: you can't just stick a raw integer or other primitive type into a Hashtable. I've been out of Java for a little while, so I'll leave it up to you to do the API reference homework for wrapping a primitive, but hopefully this will put you on the right track. Good luck!

Brian Warshaw
A: 

There is no pretty solution. Java just doesn't do things like this well. Mike's solution is pretty much the way to do it if you want strings as the indices (keys). Another option if the hash-of-hashes setup is too ugly is to append the strings together (shamelessly stolen from Mike and modified):

Hashtable<String, String> schedule = new Hashtable<String, String>();
schedule.put("A-Winter", "M");
schedule.put("A-Spring", "tTS");

and then lookup:

String val = schedule.get(group + "-" + season);

If you're unhappy with the general ugliness (and I don't blame you), put it all behind a method call:

String whenCanIWater(String group, Date date) { /* ugliness here */ }
Derek Park
A: 

@Brian Warshaw

FYI, with Java 1.5, primitives are now autoboxed to the wrapped version, so you can call it with just the primitive:

Hashtable<String, Integer> hash = new Hashtable<String, Integer>();
hash.put("key", 15); // Works from Java 1.5 on
Mike Stone
A: 

I'm not a Java programmer, but getting away from Java and just thinking in terms that are more language agnostic - a cleaner way to do it might be to use either constants or enumerated types. This should work in any langauge that supports multi-dimensional arrays.

If using named constants, where, for example:

int A = 0;
int B = 1;
int C = 2;
int D = 3;

int Spring = 0; 
int Summer = 1;
int Winter = 2; 
int Fall = 3;
...

Then the constants serve as more readable array subscripts:

schedule[A][Winter]="M";
schedule[A][Spring]="tTS";
schedule[A][Summer]="Any";
schedule[A][Fall]="tTS";
schedule[B][Winter]="t";

Using enumerated types:

enum groups
{
  A = 0,
  B = 1,
  C = 2,
  D = 3
}

enum seasons
{
  Spring = 0,
  Summer = 1,
  Fall = 2,
  Winter = 3
}
...
schedule[groups.A][seasons.Winter]="M";
schedule[groups.A][seasons.Spring]="tTS";
schedule[groups.A][seasons.Summer]="Any";
schedule[groups.A][seasons.Fall]="tTS";
schedule[groups.B][seasons.Winter]="t";
Wing
+5  A: 

Don't try to be as dynamic as PHP is. You could try to first define what you need.

interface Season
{
    public string getDays();
}

interface User
{
    public Season getWinter();
    public Season getSpring();
    public Season getSummer();
    public Season getFall();
}

interface UserMap
{
    public User getUser(string name);
}

And please, read the documentation of Hashtable before using it. This class is synchronized which means that each call is protected against multithreading which really slows the access when you don't need the extra protection. Please use any Map implementation instead like HashMap or TreeMap.

Vincent Robert
+4  A: 

It seems like everyone is trying to find the Java way to do it like you're doing it in PHP, instead of the way it ought to be done in Java. Just consider each piece of your array an object, or, at the very least, the first level of the array as an object and each sub level as variables inside the object. The build a data structure that you populate with said objects and access the objects through the data structure's given accessors.

Something like:

class Schedule
{
  private String group;
  private String season;
  private String rundays;
  public Schedule() { this.group = null; this.season = null; this.rundays= null; }
  public void setGroup(String g) { this.group = g; }
  public String getGroup() { return this.group; }
  ...
}

public ArrayList<Schedule> schedules = new ArrayList<Schedule>();
Schedule s = new Schedule();
s.setGroup(...);
...
schedules.add(s);
...

Of course that probably isn't right either. I'd make each season an object, and maybe each weekday list as an object too. Anyway, its more easily reused, understood, and extensible than a hobbled-together Hashtable that tries to imitate your PHP code. Of course, PHP has objects too, and you should use them in a similar fashion instead of your uber-arrays, wherever possible. I do understand the temptation to cheat, though. PHP makes it so easy, and so fun!

Ian Good
A: 

I agree that you should definitely put this logic behind the clean interface of:

public String lookupDays(String group, String date);

but maybe you should stick the data in a properties file. I'm not against hardcoding this data in your source files but, as you noticed, Java can be pretty wordy when it comes to nested Collections. Your file might looks like:

A.Summer=M
A.Spring=tTS
B.Summer=T

Usually I don't like to move static data like this to an external file because it increases the "distance" between the data and the code that uses it. However, whenever you're dealing with nested Collections, especially maps, things can get real ugly, real fast.

If you don't like this idea, maybe you can do something like this:

public class WaterScheduler
{
  private static final Map<String, String> GROUP2SEASON = new HashMap<String, String>();
  static
  {
    addEntry("A", "Summer", "M");
    addEntry("A", "Spring", "tTS");
    addEntry("B", "Summer", "T");
  }

  private static void addEntry(String group, String season, String value)
  {
    GROUP2SEASON.put(group + "." + season, value);
  }

}

You lose some readability but at least the data is closer to where it's going to be used.

Outlaw Programmer
+1  A: 

I'm totally at a loss as to why some of you seem to think that throwing gobs of objects at the code is the way to go. For example, there are exactly four seasons, and they don't do or store anything. How does it simplify anything to make them objects? Wing is quite right that these should probably be constants (or maybe enums).

What Bruce needs, at it's heart, is simply a lookup table. He doesn't need a hierarchy of objects and interfaces; he needs a way to look up a schedule based on a season and a group identifier. Turning things into objects only makes sense if they have responsibilities or state. If they have neither, then they are simply identifiers, and building special objects for them just makes the codebase larger.

You could build, e.g., Group objects that each contain a set of schedule strings (one for each season), but if all the Group object does is provide lookup functionality, then you've reinvented the lookup table in a much less intuitive fashion. If he has to look up the group, and then lookup the schedule, all he has is a two-step lookup table that took longer to code, is more likely to be buggy, and will be harder to maintain.

Derek Park
A: 

I think Ian is absolutely right: stop trying to implement your PHP code in Java. Instead, take a step back and think about how you might design this from scratch. In particular, why not put all that data into a database, instead of hard-coding it in your sources or using properties files? Using a database will be much easier to maintain, and there are a variety of free database engines to choose from.

Jason Day
A: 

@Jason

In particular, why not put all that data into a database, instead of hard-coding it in your sources or using properties files? Using a database will be much easier to maintain, and there are a variety of free database engines to choose from.

Adding a database is an incredibly heavyweight way to solve a problem that fits easily into a text file (or even directly into the source). There are 5 groups and 4 seasons. That means there are going to be a total of 20 records in the database.

Derek Park
A: 

@Derek

Adding a database is an incredibly heavyweight way to solve a problem that fits easily into a text file (or even directly into the source). There are 5 groups and 4 seasons. That means there are going to be a total of 20 records in the database.

Two of the database engines I linked to are implemented entirely in Java and can be embedded in an application just by including a jar file. It's a little heavyweight, sure, but it's a lot more scalable and easier to maintain. Just because there are 20 records today doesn't mean there won't be more later due to changing requirements or feature creep.

If in a few weeks or months you decide you want to add, say, time of day watering restrictions, it will be much easier to add that functionality if you're already using a database. Even if that never happens, then you've spent a few hours learning how to embed a database in an application.

Jason Day
+1  A: 

@Jason

Two of the database engines I linked to are implemented entirely in Java and can be embedded in an application just by including a jar file. It's a little heavyweight, sure, but it's a lot more scalable and easier to maintain. Just because there are 20 records today doesn't mean there won't be more later due to changing requirements or feature creep.

If in a few weeks or months you decide you want to add, say, time of day watering restrictions, it will be much easier to add that functionality if you're already using a database. Even if that never happens, then you've spent a few hours learning how to embed a database in an application.

Embedding a DB in Java doesn't make it easier to maintain. There's now an additional code dependency that did not previously exist. Updating the set of schedules is now more difficult, as either a custom tool must be coded or a DB-specific interface must be used, whereas previous notepad.exe was sufficient. Scalability is not a concern here, either. The needs of this system could increase by a million and the flat file would still work just fine.

Certainly, it's possible that in the future needs will evolve to the point that it's worth moving to a database. That's when you make the change. Trying to future-proof an application never works, because we always assume incorrectly about future needs. We can't say it'll only take a few hours to embed the database either, because we suck at estimating schedules, too. If there even comes a time when the database is appropriate, then refactor the code to use a database. In the meantime, do the simplest thing that could possibly work.

Derek Park
Both of you could discuss in comments on the first answer. Answers to the main question are not displayed in order, so it makes your discussion a bit difficult to follow...
Damien Pollet
Damien, lots of "answers" are still hanging around from the early days of Stack Overflow, when comments were not implemented (and Jeff was arguing that they were unnecessary . . . ).
Derek Park
A: 

Does the "date" have to be a parameter? If you're just showing the current watering schedule the WateringSchedule class itself can figure out what day it is, and therefore what season it is. Then just have a method which returns a map where the Key is the group letter. Something like:

public Map<String,List<String>> getGroupToScheduledDaysMap() {
  // instantiate a date or whatever to decide what Map to return
}

Then in the JSP page

<c:forEach var="day" items="${scheduler.groupToScheduledDaysMap["A"]}">
   ${day}
</c:forEach>

If you need to show the schedules for more than one season, you should have a method in the WateringSchedule class that returns a map where Seasons are the keys, and then Maps of groupToScheduledDays are the values.

bpapa
+3  A: 

Here's one way it could look like, you can figure the rest out:

A = new Group();
A.getSeason(Seasons.WINTER).addDay(Days.MONDAY);
A.getSeason(Seasons.SPRING).addDay(Days.TUESDAY).addDay(Days.THURSDAY);
A.getSeason(Seasons.SPRING).addDays(Days.MONDAY, Days.TUESDAY, ...);

schedule = new Schedule();
schedule.addWateringGroup( A );
Akira
+1  A: 

I'm with those that suggest encapsulating function in objects.

import java.util.Date;
import java.util.Map;
import java.util.Set;

public class Group {

    private String groupName;

    private Map<Season, Set<Day>> schedule;

    public String getGroupName() {
     return groupName;
    }

    public void setGroupName(String groupName) {
     this.groupName = groupName;
    }

    public Map<Season, Set<Day>> getSchedule() {
     return schedule;
    }

    public void setSchedule(Map<Season, Set<Day>> schedule) {
     this.schedule = schedule;
    }

    public String getScheduleFor(Date date) {
     Season now = Season.getSeason(date);
     Set<Day> days = schedule.get(now);
     return Day.getDaysForDisplay(days);
    }

}

EDIT: Also, your date ranges don't take leap years into account:

Our seasons look like this: Summer (5-1 to 8-31) Spring (3-1 to 4-30) Fall (9-1 to 10-31) Winter (11-1 to 2-28)

McDowell