This thread will be an attempt to develope a SIMPLE (and good enough) class
for beginning Java students, to ease their coding
for valid and crash proof, user (Console type) input,
(and also handle Console output.)

We assume here that beginning students need not worry much,
about obtaining the fastest possible IO,
with respect to the console,
in their typical beginning student type problems,

(Note that IO is often the big bootle-neck
to the total time it takes to execute a typical student type program,
since printing to screen is slow,
and there are even longer waits for a user to enter prompted data,
and thus the very significant added time to wait here,
for user to finally press the (data) Enter key.)

Thus, we are NOT overly concerned here about speed.

Simplicity and ease of understanding the code is the aim here ...

And thus, beginning coders can fairly easily adapt,
(i.e. easily add new methods),
so that this IO class will be very useful
to ease their IO coding in many,
if not most all of,
their student coding problems.

Credits:

With thanks to 'PulsarScript' for:

https://www.daniweb.com/programming/software-development/threads/506093/java-output

and to 'sarah_15' for:

https://www.daniweb.com/programming/software-development/threads/506097/java-program

and to Dani's own 'JC' for his inspiration for this thread.

Ok ... here is a simple Console class
that students might like to use
to ease and simplify handling (most) all (of) their Console IO:

// this version: 2016-09-29 //

import java.util.Scanner;

public class Console {

    private static Scanner sc = new Scanner( System.in );

    public static String takeInLine( String prompt ) {
        System.out.print( prompt );
        return sc.nextLine();
    }

    public static int takeInInt( String prompt ) {
        while( true ) {
            try {
                return Integer.parseInt( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                System.out.println( "Error! Invalid integer. Try again." );
            }
        }
    }

    public static long takeInLong( String prompt ) {
        while( true ) {
            try {
                return Long.parseLong( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                System.out.println( "Error! Invalid long. Try again." );
            }
        }
    }

    public static double takeInDouble( String prompt ) {
        while( true ) {
            try {
                return Double.parseDouble( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                System.out.println( "Error! Invalid decimal. Try again." );
            }
        }
    }

    // Call as: more( "Y" ); // to have default case of 'Yes more' //
    // Call as: more( "N" ); // to have default case of 'No more'  //
    public static boolean more( String defaultStr ) {
        String reply = takeInLine( "More (y/n) ? " );
        if( defaultStr.equalsIgnoreCase( "Y" ) ) {
            if( reply.equalsIgnoreCase( "N" ) ) {
                return false;
            } else {
                return true;
            }
        } else {
            if( reply.equalsIgnoreCase( "Y" ) ) {
                return true;
            } else {
                return false;
            }
        }
    }

    // defaults to 'Yes more' //
    public static boolean more() {
        return more( "Y" );
    }

    public static void println() {
        System.out.println();
    }

    public static void println( String s ) {
        System.out.println( s );
    }

    public static void print( String s ) {
        System.out.print( s );
    }

    public static void format( String formatStr, Object ... obj ) {
        System.out.format( formatStr, obj );
    }

}

And now a little test program to try it out:

// this version: 2016-09-29 //

class ConsoleTest {

    public static void main( String[] args ) { 

        do {
            String name   = Console.takeInLine(   "Enter this user name :  " );
            int pin       = Console.takeInInt(    "Enter pin (8 digits) :  " );
            long id       = Console.takeInLong(   "Enter ID (16 digits) :  " );
            double amount = Console.takeInDouble( "Enter $ amount owing :  " );

            Console.println();

            Console.format( "%s, %d, %d owes: %,1.2f dollars. %n", name, pin, id, amount );
            Console.println();
            Console.println( "On to next client?" );
            Console.print(   "Whenever you are ready, enter y or n, " );
            Console.println( "or just press the Enter key for 'y'." );

        } while( Console.more() );
    }
}

Recommended Answers

All 36 Replies

Here is another Dani student contributed example problem, slightly reworked here to illustrate using the above class Console ...

This example also uses the following class Investment:

public class Investment {

    private double monthlyInvestment;
    private double yearlyInterestRate;
    private int years;

    // default ctor...
    public Investment() {

        monthlyInvestment = 0.0;
        yearlyInterestRate = 0.0;
        years = 0;
    }

    // passed in values ctor...
    public Investment( double monthlyInvestment, double yearlyInterestRate, int years ) {

        this.monthlyInvestment = monthlyInvestment;
        this.yearlyInterestRate = yearlyInterestRate;
        this.years = years;
    }

    void takeIn()
    {
        monthlyInvestment  = Console.takeInDouble( "Enter next monthly investment    : " );
        yearlyInterestRate = Console.takeInDouble( "Enter this yearly interest rate  : " );
        years              = Console.takeInInt(    "Enter investment number of years : " );
    }

    public double getFutureValue() {

        double monthlyInterestRate = yearlyInterestRate/12.0/100.0;
        int months = years * 12;
        double futureValue = 0.0;
        for( int i = 1;i <= months; i++ ) {

           futureValue = (futureValue+monthlyInvestment) * (1+monthlyInterestRate);
        }
        return futureValue;
    }

    @Override
    public String toString() {

        return String.format( "Inv/Mo: monthlyInvestment(%8.2f), " +
                              "yearlyInterestRate(%5.2f%%), Years(%3d)" + 
                              "\n        Future value($%,10.2f)", 
                              monthlyInvestment, 
                              yearlyInterestRate, years,
                              getFutureValue() );
    }
}

Now the 'main' program file:

import java.util.ArrayList;

public class FutureValue {

    public static void main(String[] args) {

        // displayLine a welcome message //
        Console.println( "Welcome to the Future Value Calculator" );
        Console.println();

        ArrayList< Investment > investmentList = new ArrayList <> ();

        do{
            // get empty new investment obj.
            Investment inv = new Investment();

            // get input from user
            inv.takeIn();

            // Ok ... add this new obj. to the list ...
            investmentList.add( inv );

            Console.println();

        } while( Console.more() );

        Console.println();
        Console.println( "Ok ... NOW showing all entered info, " + 
                         "including future values ..." );

        for( Investment invObj : investmentList)
        {
            // note that calling the toString method here,
            // forces the calculation of each future value //
            Console.println( "" + invObj );
            Console.println(); 
        }

        Console.takeInLine( "All done ... press 'Enter' to continue/exit ... " );
    }
}

I’m all in favour of this idea. I’ve seen far too many posts here from beginners struggling or failing with Scanner, so an alternative is much needed.

David has has posted a whole load of code, but I'm going to ignore that for a moment and try to continue the discussion towards a consensus statement of the objectives, scope, success criteria etc

Here’s an opening suggestion:

Objective: This is an API utility for novice Java students to get simple user input in their programs.
Scope: Handle prompted String, int and double input from the system console. Handle yes/no input.
Criteria:

  1. Easiest possible use for a complete Java novice (e.g. require no knowledge of error handling).
  2. As bombproof as possible (e.g. deal sensibly with malformed input, unexpected eof).
  3. Unrecoverable errors fail hard and fast with a good explanation (e.g. throw an unchecked exception with an explicit description of the problem).
  4. API and internal code to be an example of best standard Java practice.
  5. Any extensions to the initial scope must not have any adverse impact on it.

(ps: agreed that runtime efficiency is not relevant)

Opinions?…

I suggest a more limited goal here ...

for example, there is no redirected input from file to be concerned about here,
since these takeIn 'methods' all print to console
the programmer formed prompt that guides their desired line
or (line) numeric input.

I have found these few functions (example C++ prototypes listed below):

string takeInLine( const string& prompt );
int takeInInt( const string& prompt );
double takeInDouble( const string& prompt );
bool more();

to vastly ease and speed up coding of many C, C++ and Python student type problems and facilitate crash proof input ...

and these methods can easily be enhanced
to have added passed in parameters,
to validate the range of numbers accepted for input,
if that needs to be limited.

For example:

int takeInInt( const string& prompt, int min, int max );

Here is a Python version to help illustrate this concept:

# file name: TakeInIntFloat.py #

# this version: 2016-09-30 #

def takeInInt( prompt ):
    while( True ):
        try:
            return int( input( prompt ))
        except ValueError:
            print( "Only integer numbers are accepted here." )

def takeInFloat( prompt ):
    while( True ):
        try:
            return float( input( prompt ))
        except ValueError:
            print( "Only decimal nubers are accepted here." )

def more( default="y" ):
    reply = input( "More (y/n) ? " ).lower()
    if default.lower() == "y":
        return reply != "n"
    else:
        return reply == "y"

if __name__ == "__main__":
    while True:
        name   = input(       "Enter this user name :  " )
        pin    = takeInInt(   "Enter pin (8 digits) :  " )
        id     = takeInInt(   "Enter ID (16 digits) :  " )
        amount = takeInFloat( "Enter $ amount owing :  " )

        print()

        print( "{}, {}, {} owes: ${:,.2f} dollars.".\
               format(name, pin, id, amount) )
        print()
        print( "On to next client?" )
        print( "Whenever you are ready, enter y or n, ", end="" )
        print( "or just press the Enter key for 'y'." )

        if not more():
            break

And the FutureValue.py reworked to Python 3 example, that uses (imports) the above:

# file name: Investment.py #

import TakeInIntFloat as ti  

INV_FORMAT_STR = \
    "Inv/Mo: monthlyInvestment({:8.2f}), " \
    "yearlyInterestRate({:5.2f})\%, Years({:3d})" \
    "\n        Future value(${:,.2f})"

class Investment():
    def __init__(self, monthlyInvestment=0.0, \
                 yearlyInterestRate=0.0, years=0 ):
        self.monthlyInvestment = monthlyInvestment
        self.yearlyInterestRate = yearlyInterestRate
        self.years = years

    def takeIn(self):
        self.monthlyInvestment  = ti.takeInFloat( \
            "Enter next monthly investment    : " )
        self.yearlyInterestRate = ti.takeInFloat( \
            "Enter this yearly interest rate  : " )
        self.years              = ti.takeInInt(   \
            "Enter investment number of years : " )

    def getFutureValue(self):
        monthlyInterestRate = self.yearlyInterestRate/12.0/100.0
        months = self.years * 12
        futureValue = 0.0
        for i in range( months ):
            futureValue = (futureValue + self.monthlyInvestment) * \
                          (1 + monthlyInterestRate)
        return futureValue

    def __str__(self):
        return INV_FORMAT_STR.format( self.monthlyInvestment, \
                                      self.yearlyInterestRate, \
                                      self.years, self.getFutureValue() )

if __name__ == "__main__":
    while True:
        inv = Investment()
        inv.takeIn()
        print( str(inv) )

        if not ti.more():
            break;

And:

# file name: FutureValue.py #

from Investment import Investment
from TakeInIntFloat import more

# displayLine a welcome message #
print( "Welcome to the Future Value Calculator" )
print()

investmentList = [] # get an empty Python list #

while( True ):
    inv = Investment() # get empty new investment obj #

    inv.takeIn() # get input from user #

    # Now add this new obj. to the list #
    investmentList.append( inv )

    print()     
    if not more():
        break

print()
print( "Ok ... NOW showing all entered info, " + \
       "including future values ..." );

for inv in investmentList:

    # note that calling the __str__ method here,
    # forces the calculation of each future value #
    print( str(inv) )
    print() 

input( "All done ... press 'Enter' to continue/exit ... " )

In what way is that "more limited"? The scope looks the same, and the other criteria I suggested seem implied by the objective anyway.

ps: I understand why you are posting all that code, but I doubt that anyone will take the time to read it all at this stage! "Less is more".

As a further illustration of how, in my opinion, these things should be done, here's an example of using standard JavaDoc to share the external (public) design of an API (and yes, I sneaked the input-from-a-file thing back in, but you can ignore that)

  1. Easiest possible use for a complete Java novice (e.g. require no knowledge of error handling).
  2. As bombproof as possible (e.g. deal sensibly with malformed input, unexpected eof).
  3. Unrecoverable errors fail hard and fast with a good explanation (e.g. throw an unchecked exception with an explicit description of the problem).
  4. API and internal code to be an example of best standard Java practice.
  5. Easiest possible use for a complete Java novice (e.g. require no knowledge of error handling).

I'm wondering if 1 and 5 might potentially work against 2 and 4. The goal is for the complete Java novice to be able to use this without understanding it, the public API is all they need, they need not look under the hood. Thus if you're a project manager assigning this to an experienced programmer and giving them that public API and those instructions as a spec, they'll produce the code to that spec and you and other experienced programmers judge the code based on doing what the spec requires and thinking about all the stuff that could potentially go wrong and catching all the weird stuff: ASCII, unicode, upper/lower case mix, adding extra white space, handling newlines, carriage returns, permissions, etc. Presumably you'll throw a bunch of stuff at it trying to make it break and gracefully recover or break with easy to understand by the novice error messages. You'll judge all the "under the hood" stuff as a professional non-novice, thus the "under the hood" stuff need not be understandable by novices and likely won't be.

However in THIS case, I'm figuring part of the assumption is that, as the novice progresses, he/she will take a peek under the hood and hopefully not be TOO intimidated. An example would be System.err vs. System.out for error messages. There is a difference and Java best practices would demand, I would think, that "try again, you messed up" type error messages to the user would be sent to System.out versus some other errors that would get sent to System.err (ie the user didn't mess up, the problem is elsewhere, permissions or low-level stuff). In this case you'd want, perhaps, "bad" code under the hood on purpose to oversimplify things and send everything to System.out.

We seem to have a little communication problem here ?

I am NOT at all concerned here about getting/handling any input from a file ,

neither by opening a file and reading it ...

nor by redirecting input for the program to be from a file.

That (student) task, if need and time permits,
could possibly be handled by an other FileIO type class, or by the time a student gets to file IO handl;ing, probably by directly using,
at first the scanner class,
https://docs.oracle.com/javase/tutorial/essential/io/scanning.html
than later, perhaps, by using Java's latest nio (New I O) routines.
http://www.javapractices.com/topic/TopicAction.do?Id=42

Recall these opening comments:

valid and crash proof, user (Console type) input, (and also handle Console output.)

and:

there is no redirected input from file to be concerned about here, since these takeIn 'methods' all print to console the programmer formed prompt that guides their desired line or (line) numeric input.

Thus, there exists no need here, to be concerned about handling the EOF case, since NO file handling is to be done by this class.

Note also that ...
we are here, exclusively using, Java's nextLine() calls to take in the whole line,
on ALL the prompted requests for (line) input.

This desired line input behaviour here, also includes handling,
'eating the end of line whitespace chararcters'.

(I presume the Java Scanner class nextLine() call is already well tested and safe for taking in a line and returning that line in a Java String.)

What I suggested above, was really just a slight edit to the above mentioned student supplied Console class code.

The code is NOT to be anything more than just a 'light wrapper' class,
to facilitate common student problem type requests for crash-proof and valid IO,
from the Console keyboard, by the student programmer/user.

The simple goal here is for ...
input that is very similar to that in the Python, simple to use, input function:

# take in the whole line and 'eat whitespace' at end #
lineStr = input( promptIfAnyGoesHere )

We can simply add code for exception handling
to a function with the input prompt passed in,
that has an input loop .... until NO exception raised,
for numeric types of line input.
(It seems that Java types: int, long, double, cover most? student requests for help.)

Please look again at the very simple Python:
takeInInt
and
takeInFloat
example demo'd above.

We seem to have a little communication problem here ?

I believe yes.

I am NOT at all concerned here about getting/handling any input from a file

YOU'RE not. Others may be.

but I'm going to ignore that for a moment and try to continue the discussion towards a consensus statement of the objectives, scope, success criteria etc

I took this to mean from James that this was an invitation to all to participate in some "project" if you will, going beyond the normal scope of a single thread: tutorials, code snippets, writeups, whatever folks decide it should be, aimed at novices, then tidied up, pinned, and pointed to when the need arose. So your code is basically an opening gambit into something perhaps larger and the thread has morphed. You perhaps are not interested in the morph, which is perfectly fine. This site allows for code snippets by one contributor working alone. You're welcome to create one, and I believe that is encouraged. But it doesn't look like you have it identified this thread as that.

I believe the "communication problem" is actually quite deep here. Is this a thread discussing your ideas alone and the objective is already established (newbie console input light wrapper only, done deal, that's it) or is the objective to toss around ideas of what exactly this should include (consoles, file handling, perhaps something else)? I thought it was the latter, but it appears now to be the former, so I'll bow it. Frankly you're already expanding it yourself. You have it tagged "Java" and I'm seeing a lot of Python and I saw what looks like C++ in another of your threads, so there's thread/project/idea drift right there).

This discussion is useful ... maybe even a little fruitful ...

and perhaps is typical of many little jobs getting their start ...

starting out with a fairly simple and narrow focus in mind ...

but later, perhaps, the job gets expanded, to keep the programmer(s) employed :)

But more seriously here,
I fail to see the need to expand the focus I have suggested,
since the idea (and initial student Console code presented) here ...
was just to ease absolute newbies coding Console IO,
and so that their programs would not crash on illegal types of data input.

Once a student progresses some, typically, won't they be encouraged to code their own error handling routines for file IO, using the latest Java NEW IO (nio) ?

Shalom shalom,
David

Hi guys
Sorry for the delay in replying, but my internet has been out since Saturday night.

@assertnull

I agree. As originally identified by David the goal is to make things as simple as possible for novices. Inevitably that will involve some of the implementation using non-novice code (e.g. re-throwing a checked exception as an unchecked so the user doesn’t need to handle it). As it happens, I don’t think the implementation requires anything too obscure (no generics, reflection, rmi or whatever), so whatever code it does include could be usable as an example of good practice. Of course the API itself and its documentation are what really matters.

@David

I think we’re all heading the same way here. I fully understand what your java and Python code is illustrating. I’m just trying to take your idea and help develop it into good Java with great “not crashing” behaviour. We’re going to get the best result by inviting others to comment and contribute, and by taking note of their good ideas.

Over the years I think I have fielded more novice problems with Scanner than any other Java topic. Its API is too complex for simple cases, it forces the user into catching exceptions in an inner loop when they are nowhere near ready, and has horrible gotchas like rejecting 12,000 as a number, or the infamous nextInt(), nextLine() trap. I’m into this project because it’s an opportunity to use those experiences to avoid everything that’s wrong with Scanner.

The API documented in my previous post is intended to be exactly what you proposed but with more typical Java semantics and feel. Nothing added. The rest is all about making it as bomb-proof as possible, and failing appropriately where necessary - based largely on what goes wrong with Scanner.

re file input - I deliberately did not include it in my suggested scope, and later only mentioned it as “you can ignore that”. But for the record, I wasn’t thinking of any kind of general file input. All I wanted was to encourage the programmer to create a little file of test data, exactly as would be entered at the Console, and use it to re-run decent tests frequently during development. Once again, the idea was that the programmer would be shielded as much as possible from any errors. But in any case I only suggested it as an optional extension to the scope. ) (ps EOF can happen on the console as well - ctrl-z or ctrl-d )

Just to show that I’m not going off into hyperspace with this, here’s an example of the kind of bomb-proofing and error handing that I had in mind… nothing too difficult here…

public int getInt(String prompt) {
    int errorCount = 0;
    while (errorCount++ < 3) { // no risk of an infinite loop
        String input = getString(prompt);
        input = input.trim(); // ignore leading and trailing spaces
        input = removeThousandsSeparators(input); 
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException e) {
            System.out.println("Error! Invalid integer. Try again.");
        }
    }
    // three consecutive failures - time to give up.
    throw new RuntimeException("Repeated invalid user input");
}

(removeThousandsSeparators is just a little method that uses a regex replace)

I'll use James' class name (UserInput) for this post to differentiate it from Java's Console class. What name to give this class is potentially important, particularly since it's aimed at newbies. My vote would be to give it a name that doesn't conflict with something that's already out there. I can see a newbie forgetting an import, NetBeans helpfully suggests importing the Java Console class, THEN the newbie cuts and pastes the Daniweb Console class into a separate file and nothing works because the class using it has the import line as import java.io.Console as opposed to importing the Daniweb package Console class. A newbie wouldn't know to check that.

One thing I noticed is David's code has no constructors (everything static) and James' API has constructors. So that's something to nail down. I would think that a newbie's code, assuming that we are in fact ditching the idea of reading from files, would never need multiple objects to read from the console, so either make it a static class with a private Scanner under the hood as David has it or a Singleton class.

Regarding allowing test files, consider the possiblity of this class becoming adopted by professors or a group of newbies or simply adopted by Daniweb. It's always nice to be able to say "Here's a test file" as opposed to "type all this in" and it cuts way down on the error potential. Additionally I see a very nice potential to use this class to actually log some of the console input (again, under the hood). So we're testing out whatever we are testing or we get a "Help! I'm stuck!" thread and the newbie is using this class to get input. You simply "turn logging on" and all your console input gets saved to log.txt or whatever. The newbie attaches that file to the thread (or gives it to the professor or vice versa) and you can instantly recreate exactly what the person typed in, increasing accuracy and saving time. Thus I see a lot of upsides and no downside in including the file option, if for no other reason than that. It adds no confusion at all to the newbie or anyone else. Thus, taking James' "12,000" example, it avoids typos and avoids the 20 questions game of "Did you type in '12000' or '12000.' or '12,000' or '12,000.0'?" Not difficult to implement at all (it's basically the Linux "tee" command), doesn't complicate anything, and if you don't want to use that feature, don't use it. Progressing from complete newbie to using files? Look under the hood. It could be as easy as passing the command line arguments to the UserInput class and everything is done for you.

  1. no arguments - console, no logging
  2. "log" -- write console input to default filename log.txt
  3. "log mylog.txt" -- write console input to mylog.txt
  4. "input input.txt" -- use input.txt as input rather than console.

Or even easier, add a public function to the API that prompts the user about logging, input files, output files, whatever. They type it in and presto, it's handled behind the scenes.

I agree with all that except the static bit at the beginning. Creating classes with just static members is one of those things beginners do because they haven't got the OO thing yet. If we're going to set a good example, then, in my opinion, we don't use all static.
For me the clincher is if/when you do have a test data file or a log file. These fit naturally into the idea of multiple constructors. Maybe in this exact case it's not an open and shut case, but as an example of how to think about Java classes fo me it's a no-brainer.
Yes, you can use command line arguments, but UserInput won't have the main method, so you still need a way for main to create a UserInput with those values.

ps - how about this use case:

// run multiple test cases
UserInput console = new UserInput();
do {
   String fileName = console.getString{"enter data file name");
   UserInput file = new UserInput(fileName);
   // run test case with input from file
} while (console.isYes("run more test data?");

I was thinking in terms of creating a MyPrintStream class that extended PrintStream and a MyInputStream class that extended InputStream. I was thinking along the lines of the UserInput object having a single MyInputStream member called in, which would either be System.in or read from a file, and a MyPrintStream array called out. System.out would be one of them, possibly System.err, plus any log files. Constructors and/or methods would be provided to create/set/mutate the InputStream object and the array of MyPrintStream. Default, for example would be System.in and System.out. You could pass String(s) to a constructor or setter so it read from an input file. You could have a method where it asked you a few questions and set input/output up based on the answers. For command line arguments, perhaps a method/constructor that took a String[] argument and parsed it and created the input/output streams from the parsing. It would be called from main, which could pass along those arguments.

Thus you can take any program that has System.in and System.out, and replace the part before the dot and use our class, to include David's methods and everything else InputStream provides. For output, you take everything PrintStream provides and send it to System.out, plus any files (again, like Linux's tee command).

That's the basic concept. Seems fairly easy if a tad tedious to implement (ie every override all PrintStream methods to cycle through the array of output streams rather than just one of them).

You can have the exact same program and simply change command line arguments (run your program using stdin, output the input to a log file, then next person runs the program specifying the log file as the input file.

OK, so I combined a few ideas together. I took David's code and kept it pretty much as it was except for the following...

  1. Renamed class to UserInput
  2. Changed the "more" function to a more generic takeInBoolean function.
  3. Added the option for some logging, input, and output.
  4. Made it non-static.

I have an option for an input file. If one is specified, it will be used instead of stdin. If not, stdin will be used.

For output, all input and output goes to stdout. In addition, I have two options for output files:

  1. An output file that writes everything to a file that is written to the screen.
  2. A "log" file that is designed to write all INPUT to an output file.

I did not write a function that parses command line arguments. However, I DID write a function called promptForFiles that asks for an output file, a log file, and an input file and sets those up.

I also added a function called logInput which takes a boolean and turns the logging of input on or off. The idea is that you can run your program and save the input to log.txt, then run it again and specify log.txt as the INPUT file and you should get the exact same results.

End result is anyone not wishing to use files simply does not use them and ignores them and everything associated with them.

Anyway, here it is...

package daniweb;

// this version: 2016-10-03 //

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintStream;
import java.util.Scanner;

public class UserInput {

    private Scanner consoleIn;
    private Scanner in;
    private PrintStream out; // sends all output to file
    private PrintStream log; // sends only input to file
    private boolean logInput;

    public UserInput()
    {
        consoleIn = new Scanner(System.in);
        in = consoleIn;
        out = null;
        log = null;
        logInput = false;
    }

    public String takeInLine( String prompt ) {
        print( prompt + ": ");
        try
        {
            String line = in.nextLine();
            if(in != consoleIn)
            {
                System.out.println(line);
            }
            if(out != null)
            {
                out.println(line);
            }
            if(logInput && log != null)
            {
                log.println(line);
            }
            return line;
        }
        catch(Exception ex)
        {
            if(in == consoleIn)
            {
                throw ex; // unrecoverable
            }
            else
            {
                in.close();
                in = consoleIn;
                println("Cannot read line from file.  Closing file.  Switching to console input.");
                return takeInLine(prompt); // try again
            }
        }
    }

    public int takeInInt( String prompt ) {
        while( true ) {
            try {
                return Integer.parseInt( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                println( "Error! Invalid integer. Try again." );
            }
        }
    }

    public long takeInLong( String prompt ) {
        while( true ) {
            try {
                return Long.parseLong( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                println( "Error! Invalid long. Try again." );
            }
        }
    }

    public double takeInDouble( String prompt ) {
        while( true ) {
            try {
                return Double.parseDouble( takeInLine( prompt ) );
            } catch( NumberFormatException e ) {
                println( "Error! Invalid decimal. Try again." );
            }
        }
    }

    public boolean takeInBoolean( String prompt ) {
        String reply = takeInLine( prompt + " (y/n)" );

        if( reply.equalsIgnoreCase( "N" ) ) 
        {
            return false;
        }
        else if (reply.equalsIgnoreCase( "Y"))
        {
            return true;
        }
        println("Error: Invalid.   Please enter y or n.");
        return takeInBoolean(prompt);
    }

    public void println() {
        System.out.println();
        if(out != null) out.println();
    }

    public void println( String s ) {
        System.out.println( s );
        if(out != null) out.println(s);
    }

    public void print( String s ) {
        System.out.print( s );
        if(out != null) out.print(s);
    }

    public void format( String formatStr, Object ... obj ) {
        System.out.format( formatStr, obj );
        if(out != null) out.format(formatStr, obj);
    }

    public Scanner setInputFile(String filename)
    {
        File file = new File(filename);
        try 
        {
            in = new Scanner(file);
            return in;
        }
        catch (FileNotFoundException ex) 
        {
            println(ex.toString());
            closeFiles();
            System.exit(1);
        }
        return null; // should never get here?
    }

    public PrintStream setOutputFile(String filename)
    {
        File file = new File(filename);
        try 
        {
            if(out != null) out.close();
            out = new PrintStream(file);
            return out;
        }
        catch (FileNotFoundException ex) 
        {
            println(ex.toString());
            closeFiles();
            System.exit(1);
        }
        return null;
    }

    public PrintStream setLogFile(String filename)
    {
        File file = new File(filename);
        try 
        {
            if(log != null) log.close();
            log = new PrintStream(file);
            return log;
        }
        catch (FileNotFoundException ex) 
        {
            println(ex.toString());
            System.exit(1);
        }
        return null;
    }

    public Scanner promptForInputFile()
    {
        closeInputFile();
        boolean userInputFile = takeInBoolean ("Do you have an input file");
        if(!userInputFile)
            return null;
        String filename = takeInLine("Please enter filename: ");
        return setInputFile(filename);
    }

    public PrintStream promptForOutputFile()
    {
        boolean userOutputFile = takeInBoolean ("Do you want to write all the output to a file");
        if(!userOutputFile)
            return null;
        String filename = takeInLine("Please enter output filename: ");
        return setOutputFile(filename);
    }

    public PrintStream promptForLogFile()
    {
        boolean userLogFile = takeInBoolean ("Do you want to write all the input to a log file");
        if(!userLogFile)
            return null;
        String filename = takeInLine("Please enter log filename: ");
        return setLogFile(filename);
    }

    public void promptForFiles()
    {
        promptForOutputFile();
        promptForLogFile();
        promptForInputFile();
    }

    public void closeInputFile()
    {
        if(in == consoleIn)
            return;
        in.close();
        in = consoleIn;
    }

    public void closeOutputFiles()
    {
        if(out != null)
            out.close();
        if(log != null)
            log.close();
        out = null;
        log = null;
    }

    public void closeFiles()
    {
        closeOutputFiles();
        closeInputFile();
    }

    public void logInput(boolean log)
    {
        this.logInput = log;
    }
}

And the tester...

package daniweb;

// this version: 2016-09-29 //

class ConsoleTest {

    public static void main( String[] args ) { 

        UserInput ui = new UserInput();
        ui.promptForFiles();
        ui.logInput(true); // note: if no log file set, logging will not occur

        do {
            String name   = ui.takeInLine(   "Enter this user name" );
            int pin       = ui.takeInInt(    "Enter pin (8 digits)" );
            long id       = ui.takeInLong(   "Enter ID (16 digits)" );
            double amount = ui.takeInDouble( "Enter $ amount owing" );

            ui.println();

            ui.format( "%s, %d, %d owes: %,1.2f dollars. %n", name, pin, id, amount );
            ui.println();
            ui.println( "On to next client?" );
            ui.println(   "Whenever you are ready, enter y or n" );

        } while( ui.takeInBoolean("More") );

        ui.closeFiles();
    }
}

I can't help but feel that has gone way over the top for the original spec/scope. Even without error handling its 242 lines, many of which just cover perfectly good System.out methods, but even so miss out on others that overload for all the primitive types. I think it's a mistake to try to replace System.out... why bother? It's the only class and method that everyone has already encountered in their first "hello world" program.

Here's my take - apart from the file input thing (see use case in previous post), it just does what the spec asks - user input as bullet proof as possible and if not, fails well. Easy to use, and complete with JavaDoc
(I wouldn't normally post a lot of code while discussion is still going on, but I'm away from my desk for 8 days from tomorrow, so this is my last chance.)

/**
 * This is an API utility for novice Java students to get simple user input in their programs.
 * <br> Each "get" method takes a String that is output to System.out as a prompt to the user.
 * <br> User input is parsed as tolerantly as possible. Invalid input is rejected and re-tried.
 * <br> After three consecutive failed trys a runtime exception is thrown.
 * <br> An alternative constructor allows input to be taken from a test data file.
 *
 * @author DaniWeb members
 */
public class UserInput {

    /**
     * Just some test/demo code for this class
     *
     * @param args optional input file name.
     */
    public static void main(String[] args) {
        UserInput ui = (args.length == 0) ? new UserInput() : new UserInput(args[0]);
        do {
            System.out.println("Name is " + ui.getString("Enter name: "));
            System.out.println("Age is " + ui.getInt("Enter age: "));
        } while (ui.isYes("Another?"));
    }

    private final BufferedReader br;
    private boolean echoInput = false;

    /**
     * Default constructor, user input will be taken from System.in
     */
    public UserInput() {
        br = new BufferedReader(new InputStreamReader(System.in));
    }

    /**
     * Constructor, user input will be taken from a file
     * <BR> (Intended for running repeatable test cases)
     *
     * @param inputFileName a file that replaces System.in for input
     * @throws RuntimeException an unrecoverable error occurred
     */
    public UserInput(String inputFileName) {
        // System.out.println((new File(inputFileName)).getAbsolutePath());
        try {
            br = new BufferedReader(new FileReader(inputFileName));
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
        echoInput = true;
    }

    /**
     * Asks the user to enter a string and returns the user's input
     *
     * @param prompt text to display to the user before input
     * @return the string entered by the user
     * @throws RuntimeException an unrecoverable error occurred
     */
    public String getString(String prompt) {
        System.out.print(prompt);
        String answer;
        try {
            answer = br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (answer == null) {
            throw new RuntimeException("Unexpected end of input");
        }
        if (echoInput) {
            System.out.println(answer);
        }
        return answer;
    }

    /**
     * Asks the user to enter an integer number and returns the user's input
     * <br> handles locale-dependent thousands separators correctly
     * <br> allows three tries before raising an unrecoverable error
     *
     * @param prompt text to display to the user before input
     * @return the integer value entered by the user
     * @throws RuntimeException an unrecoverable error occurred
     */
    public int getInt(String prompt) {
        int errorCount = 0;
        while (errorCount++ < 3) {  // no risk of an infinite loop
            String input = getString(prompt);
            input = input.trim(); // ignore leading and trailing spaces
            input = removeThousandsSeparators(input);
            try {
                return Integer.parseInt(input);
            } catch (NumberFormatException e) {
                System.out.println("Error! Invalid integer. Try again.");
            }
        }
        // three consecutive failures - time to give up.
        throw new RuntimeException("Repeated invalid user input");
    }

    /**
     * Like getInt - fill it in when getInt is finaised
     */
    public double getDouble(String prompt) {
        return 0.0; // should be just like getInt but double
    }

    /**
     * Asks the user to enter a yes or no and returns true for yes, false for no
     * <br> allows three tries before raising an unrecoverable error
     *
     * @param prompt text to display to the user before input
     * @return true if user enters y or yes, false for n or no.
     * <BR> All other input is rejected/retried. Input is case-insensitive
     * @throws RuntimeException an unrecoverable error occurred
     */
    public boolean isYes(String prompt) {
        int errorCount = 0;
        while (errorCount++ < 3) {
            String input = getString(prompt).toLowerCase();
            if (input.equals("y") || input.equals("yes")) {
                return true;
            }
            if (input.equals("n") || input.equals("no")) {
                return false;
            }
            System.out.println("Error! Answer yes or no");
        }
        throw new RuntimeException("Repeated invalid user input");
    }

    // utility to remove thousands separators before using parseInt
    private static final char G_S = DecimalFormatSymbols.getInstance().getGroupingSeparator();
    private static final Pattern S_P = Pattern.compile("(\\d)" + G_S + "(\\d\\d\\d)");
    private String removeThousandsSeparators(String s) {
        // remove thousands separators (comma in US/UK, period in France etc)...
        // look for (digit) group sparator (digit digit digit)
        // replace with (digit) (digit digit digit)
        return S_P.matcher(s).replaceAll( "$1$2");
    }
}

I never wanted to replace System.out, or rather PrintStream, just had been thinking you could have multiple outputs. At any rate if you look at the code in my last post, I had abandoned much of what had been in my previous post and made it simpler.

As for it being 242 lines, yeah that's a lot, never intended it to be that long. However you can pretty quickly zone out a lot of it and take out a few of the options and it gets much smaller. That code wasn't intended to be released, just an idea to kick around, have you guys run it, see what you thought as far as ease of USE for a newbie (newbie doesn't look under the hood).

At any rate, I'm not wed to the idea and completely understand if it's too far beyond the scope. No problem if it doesn't get adopted. I can always do a "one off" as my own code snippet and expand the scope, where the "official" one is unaffected.

To sum up, I imagine if Daniweb is going to make this a reality, a "project manager" for lack of a better term will have to be appointed and decide exactly where to take this, if anywhere, specify the API and official code, and pin it, and either encourage or discourage any "one-off" code. I officially volunteer myself to help if that's desired, time permitting, but frankly it's a small enough project (intentionally so) to be a one-man show.

OK. I'd also appreciate your feedback on my code if you have time - I intended it as a serious example of how to meet the objectives in good Java.
As for the "official" DaniWeb position: as moderator of the Java forum for many years, it would be me who would publish or pin the thread or the final code for future reference. However, when it comes to discussing and selecting versions no mod has any more authority than any other member - in that respect we're all equal here, with equal power to encourage or discourage and support or deprecate (provided nobody breaks the Daniweb rules). If we can't all converge and agree on something then we'll just let this thread die a nautral death, and anyone is free to post whatever of their code they believe is worth posting.
JC

I'd also appreciate your feedback on my code

Some good input (ie 4,000,000 for an integer) gets tagged as bad. It appears your code only stripped the first comma, thus turning "4,000,000" into "4000,000" and thus getting rejected because of the comma. So I made it recursive below to get the rest. In addition, when I used your function skeleton for doubles, bad stuff like "0.999,999" was being accepted, so I added a little code to not look beyond the first decimal point (see bottom of this post).

It treats strings like "4444,444" as valid integers. I'm not entirely sure what the rules are, but I thought that if you used separators, you had to use them everywhere (ie "4,444,444"). As mentioned in another thread, I'm not very good at regular expressions. I'm sure it's quite solvable with a regular expression tweak.

However, that brings up another question. Should code aimed at newbies use relatively advanced concepts like regular expressions?

I haven't tested your code exhaustively, but so far it passes other than the comma hiccups. The isYes function does not trim leading and trailing white space, so " y" and "y " are rejected. I presume they should be accepted as valid. You can add trimming in the isYes function as you do in the getInt function or you can have the getString function itself trim leading and trailing white space. Note that getString gets the entire LINE. Not a huge problem with console input, but generally a file is going to be one record per line, each field delimited by white space, so to use your example, a record would need to be on two separate lines, one line for name, one line for age as opposed to name, then white space, then age, as would be expected in most files.

The above is not a PROBLEM, just something to be noted by anyone using the file option. If you are creating test cases, you need to consider that, plus add the appropriate "yes" and "no" entries on their appropriate lines. My "logging" option in the previous code was for this purpose. The exact file to use for input could be created without thinking and without effort. All this means is that anyone creating a test file without that option has to think a little bit more about that.

One more thing. Your code correctly aborts after three attempts. Using the FILE option, it occasionally rendered the output out of order, though not often enough for me to duplicate it. Thus I would create a file with three consecutive lines of invalid input and, as it should, it threw a runtime error, but the order of the printout seemed garbled. I didn't think to copy and paste it, but I'll try to recreate it below.

Enter name: Vern
Name is Vern
Enter age: .8
Error! Invalid integer. Try again.
Enter age: .8
Error! Invalid integer. Try again.
Exception in thread "main" java.lang.RuntimeException: Repeated invalid user input
at daniweb.UserInput.getInt(UserInput.java:399)
at daniweb.UserInput.main(UserInput.java:320)
Java Result: 1
Enter age: .8
Error! Invalid integer. Try again.

I'm pretty sure that's what I saw. All I can think of is that System.err/exceptions are displayed unbuffered and System.out is buffered, so the order of words displayed to the screen is not necessarily the same as the order that commands were executed. I imagine this might drive a newbie crazy trying to debug their code, trying to trace why it displayed out of order.

"Improved" removeThousandsSeparators function is below.

private String removeThousandsSeparators(String s) 
{
    // remove thousands separators (comma in US/UK, period in France etc)...
    // look for (digit) group separator (digit digit digit)
    // replace with (digit) (digit digit digit)

    // make sure that we do not strip any separators AFTER a decimal point for doubles.
    // Note that this has no effect on any string without a decimal point.
    int dotIndex = s.indexOf(".");
    if(dotIndex != -1)
    {
        return removeThousandsSeparators(s.substring(0, dotIndex)) + s.substring(dotIndex);
    }

    String str = S_P.matcher(s).replaceAll( "$1$2");
    if(s.equals(str))
       return str; // no match.
    return removeThousandsSeparators(str); // match.  But is it the only match?  Send it through again.
}

at daniweb.UserInput.getInt(UserInput.java:399)
at daniweb.UserInput.main(UserInput.java:320)

Note: Don't worry about the line numbers. I didn't turn this into a 400 line piece of code. I just had a whole bunch of test cases that I was commenting in and out at the top.

Many thanks for that excellent feedback, and for improving the commas regex. (NB: the decimal point is also locale sensitive - eg in France it's a comma). I'm no regex expert either! I agree that regex is not for novices, but as long as they are not exposed in the public API then I see no problem.
As for the sequence of messages... ordinary output is going to System.out, but the exception is being reported on System.err. They are both going to the same device, with unspecified buffering, so exact sequence between them is uncertain.
isYes should indeed trim. I'm nervous about putting that in getString without being sure that no use case will be sensitive to leading or trailing spaces (eg indentation?)
I'm sticking to the idea that file input is only for automating console input, so multiple fields per record etc are things that I'm happy to ignore, or leave to Scanner. I'm sure David will agree.
Finally, I think I'm beginning to appreciate the thinking behind your logging idea - automatically create an input file that reproduces the current manual run's test data.
Excellent contribution all round, thank you
JC

Regarding logging and output files, I found a nice way to do it without adding any Java code. It takes a LITTLE effort, but not too bad and the result is what I had before, but with James' code instead of my longer, more complicated code, so we're on the same page, I think. Don't integrate logging/output in the main project. James' code adding the file option doesn't add much code at all, so keep that. That's my vote. Anyone wanting the ability to create test cases usable by James' code, read on. Note that I implemented James' getDouble based on his getInt functionand added an "Enter wage" option that takes a double in main.

public double getDouble(String prompt) {
    int errorCount = 0;
    while (errorCount++ < 3) {  // no risk of an infinite loop
        String input = getString(prompt);
        input = input.trim(); // ignore leading and trailing spaces
        input = removeThousandsSeparators(input);
        try {
            return Double.parseDouble(input);
        } catch (NumberFormatException e) {
            System.out.println("Error! Invalid double. Try again.");
        }
    }
    // three consecutive failures - time to give up.
    throw new RuntimeException("Repeated invalid user input");
}

New loop in main. One line added.

    do {
        System.out.println("Name is " + ui.getString("Enter name: "));
        System.out.println("Age is " + ui.getInt("Enter age: "));
        System.out.println("Wage is " + ui.getDouble("Enter wage: "));
    } while (ui.isYes("Another?"));

The whole thing reminded me of how rusty I am with my command line. The right combination of sed, diff, tr, and piping would I am sure achieve the same result, but I just ran the program from the command line, teed it to an output file called out.txt, copy and pasted the screen, and then made a little C++ program that took everything copied and pasted from the screen that WASN'T in the output file and wrote it to a file. THAT file is the exact input I typed in since what I typed was echoed to the screen, but NOT written to the output file. Note that you don't need Linux/Unix or Cygwin to run sed, diff, tr, etc. I did this on Windows 10 using MingW/MSYS. Easy/quick to install.

http://www.mingw.org/

So run James' program from the command line with the tee command so System.out goes to the screen and to an output file called out.txt.

java UserInput | tee out.txt

Copy and paste what's on the screen into a text file called screen.txt, which is everything the Java program sent to System.out plus what I typed in.

Enter name: Dan
Name is Dan
Enter age: 7
Age is 7
Enter wage: 4
Wage is 4.0
Another?y
Enter name: Fred
Name is Fred
Enter age: 2
Age is 2
Enter wage: 3
Wage is 3.0
Another?n

The file out.txt looks like this...

Enter name: Name is Dan
Enter age: Age is 7
Enter wage: Wage is 4.0
Another?Enter name: Name is Fred
Enter age: Age is 2
Enter wage: Wage is 3.0
Another?

which is everything the Java program sent to System.out, but does not include what I typed in.

What I typed in is exactly what is in screen.txt, but not in out.txt. Again, I'm sure there is a quick command line way to extract this, but I wrote a C++ program to go through screen.txt and out.txt character by character and write everything in the former but not the latter to a file called input.txt. The executable program is called getinput. On the command line, type...

getinput screen.txt out.txt input.txt

The resulting file input.txt is below, which is what I typed in when running James' program.

Dan
7
4
y
Fred
2
3
n

Run the Java program with input.txt as the input file and you get the same results as before.

java UserInput input.txt

C++ program is below.

#include <fstream>
#include <iostream>
#include <cstdlib>
using namespace std;

int main(int argc, char* argv[])
{
    if(argc != 4)
    {
        cout << "Usage: ./getinput screen_output.txt piped_output.txt java_input.txt\n";
        exit(1);
    }

    ifstream screen_output(argv[1]);
    ifstream piped_output(argv[2]);
    ofstream java_input(argv[3]);

    char screen_char, piped_char;

    piped_char = piped_output.peek();
    while(true)
    {
        screen_char = screen_output.get();
        if(!screen_output.good())
            break;

        if(screen_char != piped_char)
        {
            java_input.put(screen_char);
            continue;
        }

        if(piped_char != EOF)
        {
            piped_output.get(); // throw character away and advance
            piped_char = piped_output.peek(); 
        }            
    }

    screen_output.close();
    piped_output.close();
    java_input.close();
    return 0;
}

A couple more thoughts on logging...
If we want to do it it's a single println echoing the user input in getString, plus some trivial method to specify the output file.
However, thinking about it... its only going to be useful if the user enters a complete set of correct data, but while their code is under development they won't be able to do that because their code will crash first. So I'm happy to let them create their test data in an editor.

Since you made those improvements and I don't have a Java IDE for the next week it seems you are de-facto guardian of the latest source.
So maybe you can add a small thing I thought of. Novices get messed up with files because they don't understand the default locations that Java uses for simple file names. So in the constructor with the file name I would like to print the file's absolute path by way of confirmation of exactly what's going on. That's just un-commenting the debug print in my code and adding a bit of text like "running with input data from ”+file.getAbsolutePath()

Have fun!
Jc

OK, pretty sure this is everything. Sounds like you want logging and you're right, not much to it, so I integrated it into your code, so I changed the constructor to take a String and a boolean (filename and true if logging, false if not). If logging, take input from System.in, output to file. If NOT logging, read input from file. Default constructor remains the same. Command line is the same, just add the word "log" now after the filename for logging (actually didn't check whether the argument was "log", just assume logging if there is ANYTHING there). Changes are...

  1. No longer assume '.' as decimal separator in the removeThousandsSeparators function.
  2. Trimmed leading and trailing white space in isYes function.
  3. Added logging option back in (easy to take out again, but doesn't add much more code).
  4. Fixed a few typos, added/fixed a few comments.
  5. Added the file location printout back in.

However, thinking about it... its only going to be useful if the user enters a complete set of correct data

It's exactly as you input it and the results are the same, so if you enter with no errors, then run the input file, you'll get no errors. If you have three errors in a row in the input file, it'll abort, just like when you type it in. If you have two errors in the file, it will read it, reject it with the error message, then read it again, reject it again, then read the correct input and use it. Three bad in a row from the file? Abort, just like if you typed it in. That's how I tested the logging and reading options. And you can edit the input file to either correct bad inputs or vice versa. Logging seems useful, but again, really easy to take out. Actual code is only a few more lines. I'm fine with taking it out again.

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.text.DecimalFormatSymbols;
import java.util.regex.Pattern;

/**
 * This is an API utility for novice Java students to get simple user input in their programs.
 * <br> Each "get" method takes a String that is output to System.out as a prompt to the user.
 * <br> User input is parsed as tolerantly as possible. Invalid input is rejected and re-tried.
 * <br> After three consecutive failed tries, a runtime exception is thrown.
 * <br> An alternative constructor allows input to be read FROM a test data file or console input to be written TO a test file
 *
 * @author DaniWeb members
 */
public class UserInput {

    /**
     * Just some test/demo code for this class
     *
     * @param args optional filename.  If no second parameter, read as input.  If "log" (or anything) as second parameter, WRITE to filename
     */
    public static void main(String[] args) {
        // no arguments means no files.
        // first argument is filename, if any.
        // if there is no second argument, use the file as input.
        // If there IS a second argument, use System.in a input and write that input as output to a file for test cases.
        UserInput ui = (args.length == 0) ? new UserInput() : 
                new UserInput(args[0], (args.length > 1));
        do {
            System.out.println("Name is " + ui.getString("Enter name: "));
            System.out.println("Age is " + ui.getInt("Enter age: "));
            System.out.println("Wage is " + ui.getDouble("Enter wage: "));
        } while (ui.isYes("Another?"));
    }

    private final BufferedReader br;
    private boolean echoInput = false;   // true if we are reading from or writing to a file
    private PrintStream echoedInputFile = System.out; // use to log the input IF we are logging.  Otherwise System.out (screen)

    /**
     * Default constructor, user input will be taken from System.in
     */
    public UserInput() {
        br = new BufferedReader(new InputStreamReader(System.in));
    }

    /**
     * Constructor, user input will be taken from a file
     * <BR> (Intended for running repeatable test cases)
     *
     * @param filename a file to read from or write to
     * @param logging true if the file is a file to WRITE TO, false if the file is a file to READ FROM
     * @throws RuntimeException an unrecoverable error occurred
     */
    public UserInput(String filename, boolean logging) {
        String displayInfo = logging ? "Writing to file " : "Reading from file ";
        File file = new File(filename);
        displayInfo += file.getAbsolutePath();
        System.out.println(displayInfo);
        try {
            if(logging){
                echoedInputFile = new PrintStream(file);
                br = new BufferedReader(new InputStreamReader(System.in));
            }
            else
            {
                br = new BufferedReader(new FileReader(file));
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex);
        }
        echoInput = true;
    }

    /**
     * Asks the user to enter a string and returns the user's input
     *
     * @param prompt text to display to the user before input
     * @return the string entered by the user
     * @throws RuntimeException an unrecoverable error occurred
     */
    public String getString(String prompt) {
        System.out.print(prompt);
        String answer;
        try {
            answer = br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (answer == null) {
            throw new RuntimeException("Unexpected end of input");
        }
        if (echoInput) {
            echoedInputFile.println(answer); // will be System.out unless logging
        }
        return answer;
    }

    /**
     * Asks the user to enter an integer number and returns the user's input
     * <br> handles locale-dependent thousands separators correctly
     * <br> allows three tries before raising an unrecoverable error
     *
     * @param prompt text to display to the user before input
     * @return the integer value entered by the user
     * @throws RuntimeException an unrecoverable error occurred
     */
    public int getInt(String prompt) {
        int errorCount = 0;
        while (errorCount++ < 3) {  // no risk of an infinite loop
            String input = getString(prompt);
            input = input.trim(); // ignore leading and trailing spaces
            input = removeThousandsSeparators(input);
            try {
                return Integer.parseInt(input);
            } catch (NumberFormatException e) {
                System.out.println("Error! Invalid integer. Try again.");
            }
        }
        // three consecutive failures - time to give up.
        throw new RuntimeException("Repeated invalid user input");
    }

    /**
     * Asks the user to enter a double number and returns the user's input
     * <br> handles locale-dependent thousands separators correctly
     * <br> allows three tries before raising an unrecoverable error
     *
     * @param prompt text to display to the user before input
     * @return the double value entered by the user
     * @throws RuntimeException an unrecoverable error occurred
     */
    public double getDouble(String prompt) {
        int errorCount = 0;
        while (errorCount++ < 3) {  // no risk of an infinite loop
            String input = getString(prompt);
            input = input.trim(); // ignore leading and trailing spaces
            input = removeThousandsSeparators(input);
            try {
                return Double.parseDouble(input);
            } catch (NumberFormatException e) {
                System.out.println("Error! Invalid double. Try again.");
            }
        }
        // three consecutive failures - time to give up.
        throw new RuntimeException("Repeated invalid user input");
    }

    /**
     * Asks the user to enter a yes or no and returns true for yes, false for no
     * <br> allows three tries before raising an unrecoverable error
     *
     * @param prompt text to display to the user before input
     * @return true if user enters y or yes, false for n or no.
     * <BR> All other input is rejected/retried. Input is case-insensitive
     * @throws RuntimeException an unrecoverable error occurred
     */
    public boolean isYes(String prompt) {
        int errorCount = 0;
        while (errorCount++ < 3) {
            String input = getString(prompt).toLowerCase();
            input = input.trim(); // ignore leading and trailing spaces
            if (input.equals("y") || input.equals("yes")) {
                return true;
            }
            if (input.equals("n") || input.equals("no")) {
                return false;
            }
            System.out.println("Error! Answer yes or no");
        }
        throw new RuntimeException("Repeated invalid user input");
    }

    // utility to remove thousands separators before using parseInt
    private static final char G_S = DecimalFormatSymbols.getInstance().getGroupingSeparator();
    private static final char D_S = DecimalFormatSymbols.getInstance().getDecimalSeparator();
    private static final Pattern S_P = Pattern.compile("(\\d)" + G_S + "(\\d\\d\\d)");
        private String removeThousandsSeparators(String s) 
        {
            // remove thousands separators (comma in US/UK, period in France etc)...
            // look for (digit) group separator (digit digit digit)
            // replace with (digit) (digit digit digit)

            // make sure that we do not strip any separators AFTER a decimal for doubles.
            // Note that this has no effect on any string without a decimal.
            int decimalIndex = s.indexOf(D_S);
            if(decimalIndex != -1)
            {
                return removeThousandsSeparators(s.substring(0, decimalIndex)) + s.substring(decimalIndex);
            }

            String str = S_P.matcher(s).replaceAll( "$1$2");
            if(s.equals(str))
               return str; // no match.
            return removeThousandsSeparators(str); // match.  But is it the only match?  Send it through again.
        }
}

Sounds like you want logging

I re-read your post, might have misread it before.

One more thought. Regarding input files, you have the boolean echoInput variable. If you take all the file stuff out and set echoInput true, you can run this from the command line to use input.txt as an input file without any files.

java UserInput < input.txt

Same as what we had before without the < symbol.

Ok, good work!
Re logging: right now I'm against it and see it as an unnecessary complication. I'm especially against messing up the second constructor with a cryptic Boolean to support it. I would much prefer to leave that constructor alone and add a separate method to set a log file for the (few?) people who may use it, e.g.createDataFile(file)
As I said before, by the time our novice has got his code to the point where he can enter all the data without crashing, it's too late to be really useful.

Edit: after yet more thought here's how I would now do it...
Always append all user input to a List<String> as it is entered. Then createDataFile() opens a standard save dialog and writes the contents to the file.
So the use case is:
User does test run. Gets to end. Decides it's worth keeping this data. Creates data file.
... which makes more sense to me.

Referring back to my 242 liner here...

https://www.daniweb.com/programming/software-development/threads/506147/developing-a-class-to-ease-student-coding-crash-proof-and-valid-io#post2210153

I had a setLogFile function(String) and opened that file for logging and a logInput(boolean) that turned logging on and off, so I think that solves the constructor problem. Revert the constructor back to pre-logging where the String takes the input filename.

Regarding your List of Strings logging solution where you add to the list , you can combine those two functions, get rid of the "starting and stopping" logging idea, and just go with a single writeInputToFile(String filename) function that opens the file, writes the List to it, and closes. Done.

I also added a setter to turn logging on and off and to clear it. Yes, it adds a little bit to the code length, but I don't think it COMPLICATES anything. I actually think it simplifies things. Again, easy to take out and I have no problem with you taking it out. I think it addresses your concerns though. I think we're going full circle here a little bit. Part of my idea for logging was for the Daniweb newbies asking questions, this allows them to log the exact input for any thread they have so the NON-newbie question answerers can duplicate it exactly and detect any bugs.

I tried to ATTACH the code since this thread is getting crowded with code (a little Subversion system would be quite nice here with all the different verions!), but it won't let me attach a java file, so I'll hold off on that a little bit.

Yes, I think you and I have converged nicely. What we really need now is some more input.
Jc

Reading all this again I'm not sure where we stand on the logging thing... did you agree with my suggestion of adding everything to a List and having a method to save its contents? Did you keep the start/stop logging thing, and if so, what's the use case for that?

If you pm me your email address I'll set up a Dropbox shared folder where we can sync the full code and only post stuff here where it's new or interesting. No hurry, I'm here on hold until middle next week with no IDE or compiler.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.