tags:

views:

672

answers:

8
/*This is a program that calculates Internet advertising rates based on what features/options you choose.
 * 
 *  
 */

import java.util.Scanner;

public class InternetAdvertising 
{
    public static void main(String[] args)
    {
     Scanner in = new Scanner(System.in);

     int numberOfWords;  

     //I assigned 0 values to both as Eclipse suggested
     float textCost = 0;
     float linkCost = 0;  

     float graphicCost;

     //<=25 words is a flat fee of $.40 per word plus Base fee of $3.00 
     final float TEXT_FLAT_FEE = 0.40F;
     final float TEXT_BASE_FEE = 3.00F;

     //<=35 words is $.40 for the first 25 words and 
     //an additional $.35 per word up to and including 35 words plus Base fee of $3.00 
     final float LESS_OR_EQUAL_THAN_THIRTYFIVE = 0.35F;

     //Over 35 words is a flat fee of $.32 per word with no base fee
     final float MORE_THAN_THIRTYFIVE = 0.32F;


     System.out.println("Welcome!");

     System.out.print("Enter the number of words in your ad: ");
     numberOfWords = in.nextInt();

     if (numberOfWords <= 25)
     {
      textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * numberOfWords);
     }

     else if (numberOfWords <= 35)
     {
      textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * 25) + (numberOfWords - 25) * LESS_OR_EQUAL_THAN_THIRTYFIVE;
     }

     else if (numberOfWords > 35)
     {
      textCost = numberOfWords * MORE_THAN_THIRTYFIVE;
     }


     String addLink, advancePay;
     char link, advPay;

     final float LINK_FLAT_FEE = 14.95F;
     final float THREE_MONTH_ADV_DISCOUNT = 0.10F;

     System.out.print("Would you like to add a link (y = yes or n = no)? ");
     addLink = in.next();

     link = addLink.charAt(0);
     link = Character.toLowerCase(link); 

     if (link == 'y')
     {
      System.out.print("Would you like to pay 3 months in advance " + "(y = yes or n = no)? ");
      advancePay = in.next();

      advPay = advancePay.charAt(0);
      advPay = Character.toLowerCase(advPay);

      switch (advPay)
      {
       case 'y':

        linkCost = (3 * LINK_FLAT_FEE) - (3 * LINK_FLAT_FEE) * THREE_MONTH_ADV_DISCOUNT;

        break;

       case 'n':

        linkCost = LINK_FLAT_FEE;

        break;
      }    
     }

     else
     {
      linkCost = 0;
     }


     String addGraphic;
     char graphic;

     System.out.print("Would you like to add graphics/pictures” + “(S = Small, M = Medium, L = Large or N = None)? ");
     addGraphic = in.next();

     graphic = addGraphic.charAt(0);
     graphic = Character.toUpperCase(graphic);
     graphic = Character.toLowerCase(graphic);  
     switch (graphic)
     {
      case 's':

       graphicCost = 19.07F;

       break;

      case 'm':

       graphicCost = 24.76F;

       break;

      case 'l':

       graphicCost = 29.33F;

       break;

      default:
       graphicCost = 0;
     }


     float gst, totalBeforeGst, totalAfterGst;

     final float GST_RATE = 0.05F;

     totalBeforeGst = textCost + linkCost + graphicCost; //textCost & linkCost would not initialize

     gst = totalBeforeGst * GST_RATE;

     totalAfterGst = totalBeforeGst + (totalBeforeGst * GST_RATE);


     System.out.printf("\t\t%-16s %11s\n", "Category", "Cost");
     System.out.printf("\t\t%-16s %11.2f\n", "Text", textCost);  //linkCost would not initialize
     System.out.printf("\t\t%-16s %11.2f\n", "Link", linkCost); //textCost would not initialize 
     System.out.printf("\t\t%-16s %11.2f\n", "Graphic", graphicCost);
     System.out.printf("\t\t%-16s %11.2f\n", "Total", totalBeforeGst);
     System.out.printf("\t\t%-16s %11.2f\n", "GST", gst);
     System.out.printf("\t\t%-16s %11.2f\n", "Total with GST", totalAfterGst);
    } 
}

I'm almost done with this code and Eclipse suggests that I assign 0 values to textCost and linkCost. Is there any other way to go around this problem. If I don't assign 0 values they get an error (The local variable XXX may not have been initialized). Can someone explain to me why this happens even though I have both variables assigned with equations?

Thanks.

EDIT: I did as suggested and declared the variables only when I'm going to need it. I also added some comments.

+3  A: 

this happens because the assignment occurs inside a conditional, if the condition is not met, the assignment never occurs

to avoid the error you have to assign a value (the most common would be 0) outside the conditional.

josemrb
+2  A: 

Three suggestions before I delve any deeper into the code:

  • Declare variables as late as you can to make it easier to understand the code.
  • Refactor this giant method - it's unreadably huge at the moment.
  • Make the constants static final fields. They're not related to any particular call to the method, so they shouldn't be local variables.

Now as to the actual question, the simplest way is to make sure that every possible flow actually does assign a value or throw an exception. So for textCost, change your code to:

if (numberOfWords <= 25)
{
    textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * numberOfWords);
}
else if (numberOfWords <= 35)
{
    textCost = TEXT_BASE_FEE + (TEXT_FLAT_FEE * 25) + (numberOfWords - 25) * 
               LESS_OR_EQUAL_THAN_THIRTYFIVE;
}
else // Note - no condition.
{
    textCost = numberOfWords * MORE_THAN_THIRTYFIVE;
}

For linkCost, change your switch statement to something like:

switch (advPay)
{
    case 'y':
        linkCost = (3 * LINK_FLAT_FEE) - 
                   (3 * LINK_FLAT_FEE) * THREE_MONTH_ADV_DISCOUNT;
        break;
    case 'n':
        linkCost = LINK_FLAT_FEE;
        break;
    default:
        throw new Exception("Invalid value specified: " + advPay);
}

Now you may not want to throw an exception here. You might want to loop round again, or something like that. You probably don't want to use just bare Exception - but you should think about the exact exception type you do want to use.

It's not always possible to do this. The rules by the compiler to determine definite assignment are relatively straightforward. In cases where you really can't change the code to make the compiler happy like this, you can just assign a dummy initial value. I'd recommend trying to avoid this wherever possible though. In your first case, the value really would always be assigned - but in the second case you really weren't giving a value when advPay was neither 'y' nor 'n' which could lead to a hard-to-diagnose problem later on. The compiler error helps you spot this sort of problem.

Again though, I strongly suggest you refactor this method. I suspect you'll find it a lot easier to understand why things aren't definitely assigned when there's only about 10 lines of code to reason about in each method, and when each variable is declared just before or at its first use.

EDIT:

Okay, the radically refactored code is below. I'm not going to claim it's the best code in the world, but:

  • It's more testable. You could easily write unit tests for each part of it. printAllCosts isn't terribly easily testable, but you could have an overload which took a Writer to print to - that would help.
  • Each bit of calculation is in a logical place. Links and graphics have a small set of possible values - Java enums are a natural fit here. (I'm aware they may well be beyond your current skill level, but it's good to see what will be available.)
  • I'm not using binary floating point numbers any more, because they're inappropriate for numbers. Instead, I'm using an integer number of cents everywhere and converting to BigDecimal for display purposes. See my article on .NET floating point for more information - it's all relevant to Java really.
  • The advert itself is now encapsulated in a class. You could add a lot more information here as and when you needed to.
  • The code is in a package. Admittedly it's all in one file at the moment (which is why only the EntryPoint class is public) but that's just for the sake of Stack Overflow and me not having to open up Eclipse.
  • There's JavaDoc explaining what's going on - at least for a few methods. (I would probably add some more in real code. I'm running out of time here.)
  • We validate user input, except for the word count - and we perform that validation in a single routine, which should be reasonably testable. We can then assume that whenever we've asked for input, we've got something valid.
  • The number of static methods in EntryPoint is slightly alarming. It doesn't feel terribly OO - but I find that's often the way around the entry point to a program. Note that there's nothing to do with fees in there - it's just the user interface, basically.

There's more code here than there was before - but it's (IMO) much more readable and maintainable code.

package advertising;

import java.util.Scanner;
import java.math.BigDecimal;

/** The graphic style of an advert. */
enum Graphic
{
    NONE(0),
    SMALL(1907),
    MEDIUM(2476),
    LARGE(2933);

    private final int cost;

    private Graphic(int cost)
    {
        this.cost = cost;
    }

    /** Returns the cost in cents. */
    public int getCost()
    {
        return cost;
    }
}

/** The link payment plan for an advert. */
enum LinkPlan
{
    NONE(0),
    PREPAID(1495), // 1 month
    POSTPAID(1495 * 3 - (1495 * 3) / 10); // 10% discount for 3 months up-front

    private final int cost;

    private LinkPlan(int cost)
    {
        this.cost = cost;
    }

    /** Returns the cost in cents. */
    public int getCost()
    {
        return cost;
    }
}

class Advertisement
{
    private final int wordCount;
    private final LinkPlan linkPlan;
    private final Graphic graphic;

    public Advertisement(int wordCount, LinkPlan linkPlan, Graphic graphic)
    {
        this.wordCount = wordCount;
        this.linkPlan = linkPlan;
        this.graphic = graphic;
    }

    /**
     * Returns the fee for the words in the advert, in cents.
     * 
     * For up to 25 words, there's a flat fee of 40c per word and a base fee
     * of $3.00.
     * 
     * For 26-35 words inclusive, the fee for the first 25 words is as before,
     * but the per-word fee goes down to 35c for words 26-35.
     * 
     * For more than 35 words, there's a flat fee of 32c per word, and no
     * base fee.     
     */
    public int getWordCost()
    {
        if (wordCount > 35)
        {
            return 32 * wordCount;
        }
        // Apply flat fee always, then up to 25 words at 40 cents,
        // then the rest at 35 cents.
        return 300 + Math.min(wordCount, 25) * 40
                   + Math.min(wordCount - 25, 0) * 35;        
    }

    /**
     * Displays the costs associated with this advert.
     */
    public void printAllCosts()
    {
        System.out.printf("\t\t%-16s %11s\n", "Category", "Cost");
        printCost("Text", getWordCost());
        printCost("Link", linkPlan.getCost());
        printCost("Graphic", graphic.getCost());
        int total = getWordCost() + linkPlan.getCost() + graphic.getCost();
        printCost("Total", total);
        int gst = total / 20;
        printCost("GST", gst);
        printCost("Total with GST", total + gst);
    }

    private void printCost(String category, int cents)
    {
        BigDecimal dollars = new BigDecimal(cents).scaleByPowerOfTen(-2);
        System.out.printf("\t\t%-16s %11.2f\n", category, dollars);
    }
}

/**
 * The entry point for the program - takes user input, builds an 
 * Advertisement, and displays its cost.
 */
public class EntryPoint
{
    public static void main(String[] args)
    {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Welcome!");
        int wordCount = readWordCount(scanner);
        LinkPlan linkPlan = readLinkPlan(scanner);
        Graphic graphic = readGraphic(scanner);

        Advertisement advert = new Advertisement(wordCount, linkPlan, graphic);
        advert.printAllCosts();
    }

    private static int readWordCount(Scanner scanner)
    {
        System.out.print("Enter the number of words in your ad: ");
        // Could add validation code in here
        return scanner.nextInt();
    }

    private static LinkPlan readLinkPlan(Scanner scanner)
    {
        System.out.print("Would you like to add a link (y = yes or n = no)? ");
        char addLink = readSingleCharacter(scanner, "yn");
        LinkPlan linkPlan;
        if (addLink == 'n')
        {
            return LinkPlan.NONE;
        }
        System.out.print("Would you like to pay 3 months in advance " +
                         "(y = yes or n = no)? ");
        char advancePay = readSingleCharacter(scanner, "yn");
        return advancePay == 'y' ? LinkPlan.PREPAID : LinkPlan.POSTPAID;
    }

    private static Graphic readGraphic(Scanner scanner)
    {
        System.out.print("Would you like to add graphics/pictures? " +
            "(s = small, m = medium, l = large or n = None)? ");
        char graphic = readSingleCharacter(scanner, "smln");
        switch (graphic)
        {
            case 's': return Graphic.SMALL;
            case 'm': return Graphic.MEDIUM;
            case 'l': return Graphic.LARGE;
            case 'n': return Graphic.NONE;
            default:
                throw new IllegalStateException("Unexpected state; graphic=" +
                                                graphic);
        }
    }

    private static char readSingleCharacter(Scanner scanner,
                                            String validOptions)
    {
        while(true)
        {
            String input = scanner.next();
            if (input.length() != 1 || !validOptions.contains(input))
            {
                System.out.print("Invalid value. Please try again: ");
                continue;
            }
            return input.charAt(0);
        }
    }
}
Jon Skeet
I don't think, thats both suggestions are good ideas. The warning that a variable has not been declared ist important to prevent from ur (uninitialized, read) failures. The cyclomatic number of the method is, by my count, 7 so the function is also not unnacacary big.
Red33mer
Cyclomatic complexity isn't the be-all-and-end-all of readability. The method is 125 lines long, and I'm sure it can be broken up *very* easily. Declaring variables as late as possible doesn't remove the warning about uninitialized variables - it just makes that warning easy to understand. I stand by both of my suggestions.
Jon Skeet
... and now I'm adding a third.
Jon Skeet
Another reason for splitting this up is for testability. The current method is basically untestable. (Yes, you *could* do it, but it would be insane.) Splitting the the method up into logical chunks to work out each value separately from its input data will make it *much* easier to test.
Jon Skeet
Thanks, Jon. It's been only a month since I started learning. So some of the suggestion still sound a bit alien to me but it's good that I'm learning something new. I still don't know how to create multiple methods and how I should split them properly.
HelloWorld
@HelloWorld - don't worry about it, you'll get there. But I would *seriously* suggest you start to learn more about creating other methods and types before you go much further. In particular, don't try to start doing "real" programming for useful things until you know some more of the basics.
Jon Skeet
Would it help if I tried to take your sample code and refactor it to show you what it could look like?
Jon Skeet
Yes, that would be GREAT.
HelloWorld
A: 

Hi,

the error message tells you, that those variables aren't always initialized. This is, because your initialization happens only under certain conditions (they are located in if-statements). Hope this helps..

andyp
+2  A: 

The analysis Eclipse performs to determine whether the variable is assigned on every code path isn't intelligent enough to realize that the tests on numberOfWords will never all fail. Typically, because it's not possible to statically evaluate every possible condition, a compiler/syntax checker won't attempt to evaluate any of them. If you replaced the last "else if" with an "else" it should work, as at least one assignment to textCost will occur regardless of the conditions being tested.

Nicholas Riley
I did as you suggested for textCost and replaced else if with else. Thanks.
HelloWorld
A: 

Even though you know one of the 3 branches of the comparison with numberOfWords will be visited, the compiler doesn't know that. It will wrongly assume that it is possible to enter the else clause and the textCost variable will remain unitiliazed.

Similarly with the switch (advPay). Even though you know that one of the two will be visited, the compiler doesn't.

Suggestion: Remove the else if (numberOfWords > 35) make it just an else.

As for the switch (advPay), add a default case. Inside you can put a throw new AssertionError();.

RichN
Would it be better then if I just remove the switch statement and just create a nested if statement?
HelloWorld
Doesn't really make any difference. You still need an else clause, though.
RichN
+2  A: 

Eclipse is warning you because your initializations are happening inside conditionals. If none of the conditions are satisfied, textCost will be uninitialized.

if (numberOfWords <= 25)
    {
            //assign a value to textCost
    }
    else if (numberOfWords <= 35)
    {
            //assign a value to textCost
    }
    else if (numberOfWords > 35)
    {
            //assign a value to textCost
    }

Eclipse probably isn't recognizing that (numberOfWords <= 35) and (numberOfWords > 35) cover all possibilities.

You could either initialize it to 0 on declaration, or include an additional else {} which sets it to zero.

Similar explanation for the other variable.

hexium
Think you meant to say (numberOfWords <= 35) and (numberOfWords > 35). Damn copy/paste ;)
ThisSuitIsBlackNot
Thanks, I noticed that too and was fixing it as you replied. :)
hexium
Also, you could replace the `else if (numberOfWords > 35)` with just `else`. And Eclipse probably isn't allowed to recognize that all the possibilities are covered; Java actually has specified rules on when to emit a "may not have been initialized", and this case might hit those.
jk
+4  A: 

linkCost is not initialized when link == 'y' and advPay is not 'y' or 'n'.

In other words, you get this error when the compiler can find a path through your code where a local variable is not initialized before it is used.

eljenso
A: 

Did you try Ctrl-1 ón the warning location to see what Eclipse suggested you could dó?

Thorbjørn Ravn Andersen