4
\$\begingroup\$

Been working on a BMR calculator on and off for a while now. Created an interface and been over different ways to store user input, different ways to calculate BMR/TDEE etc.

I created another class called User for the calculations (as suggested by someone). This way I can pack the data of a user together with any methods that use the data.

Any criticism is appreciated. Thanks

/** 
 * @author "Faizan Tahir"
 * @title "BMR Calculator"
 * A program to calculate a users BMR (Basal metabolic rate) and TDEE (Total Daily Energy Expenditure). 
 * Allows user to select how they would like to input their information (e.g. weight in kg or cm),
 * Calculates users TDEE given an activity level. Basic and straightforward to use. User does not need to convert
 * to their standard unit of measure, this calculator does that for them.
 * Future updates: Allow user to save their information so as to track fitness progress
 **/


import javax.swing.*;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; 
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.util.HashMap;


public class BmrMain extends JFrame {



    // Declarations for gender/weight/height and other variables
    HashMap<String, Double> activityMap;
    String[] activityLevels = {"Sedentary", "Lightly Active", "Moderately Active", "Very Active", "Extra Active"};


    int userAge;
    String userGender;
    double userHeight;
    double userWeight;
    double userActivityLevel;


    public BmrMain(String title) {

        // Main JFrame
        setTitle(title);
        JPanel mainPanel = new JPanel();

        // All JPanel declarations
        JMenuBar menuBar = new JMenuBar();
        JPanel imgPanel = new JPanel();
        JPanel agePanel = new JPanel();
        JPanel genderPanel = new JPanel();
        JPanel heightPanel = new JPanel();
        JPanel weightPanel = new JPanel();
        JPanel weightHeightPanel = new JPanel(new BorderLayout());// combines genderPanel, agePanel, heightPanel, weightPanel - this
                                                                  // is a BorderLayout, whereas gender/agePanel are FlowLayouts.
        JPanel combinedGAHWpanel = new JPanel(new BorderLayout()); // Combines weight and height panel into out flowlayout panel, which is then combined into the above borderlayout panel
        JPanel tdeeBMRPanel = new JPanel(new BorderLayout());
        JPanel tdeePanel = new JPanel();
        JPanel activityLevelPanel = new JPanel();
        JPanel bmrTDEEValuesPanel = new JPanel(new BorderLayout());

        // Image panel declaration
        JLabel imgLabel = new JLabel(new ImageIcon(getClass().getResource("/mainlogo2.png")));
        JLabel activityLevelHelp = new JLabel(new ImageIcon(getClass().getResource("/question-mark.png")));
        imgPanel.add(imgLabel);

        // Activity level map
        initializeActivityLevelMap();


        // JPanel layout managers
        agePanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));
        genderPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 5, 5));


        // Menu JComponents
        JMenu saveMenu = new JMenu("Save");
        JMenu optionMenu = new JMenu("Options");
        JMenu helpMenu = new JMenu("Help");
        menuBar.add(saveMenu);
        menuBar.add(optionMenu);
        menuBar.add(helpMenu);

        // Age JComponents
        JLabel ageLabel = new JLabel("Age:");
        JLabel yearsLabel = new JLabel("<html><i>years</i><html>");
        JTextField ageField = new JTextField(4);

        agePanel.add(ageLabel);
        agePanel.add(ageField);
        agePanel.add(yearsLabel);

        // Gender JComponents
        JLabel genderLabel = new JLabel("Gender:");
        JRadioButton genderMale = new JRadioButton("Male", true);
        JRadioButton genderFemale = new JRadioButton("Female");



        genderPanel.add(genderLabel);
        genderPanel.add(genderMale);
        genderPanel.add(genderFemale);


        ButtonGroup genderGroup = new ButtonGroup(); // groups male and female radio buttons together so that only one can be selected
        genderGroup.add(genderMale);
        genderGroup.add(genderFemale);



        // Height JComponents
        JLabel heightLabel = new JLabel("Height:");
        JTextField height1Field = new JTextField(3);
        JTextField height2Field = new JTextField(3);
        JLabel height1Label = new JLabel("m");
        JLabel height2Label = new JLabel("cm ");
        JToggleButton cmButton = new JToggleButton("cm", true);
        JToggleButton feetButton = new JToggleButton("feet");
        heightPanel.add(heightLabel);


        ButtonGroup heightGroup = new ButtonGroup();
        heightGroup.add(cmButton);
        heightGroup.add(feetButton);


        heightPanel.add(height1Field);
        heightPanel.add(height1Label);
        heightPanel.add(height2Field);
        heightPanel.add(height2Label);
        heightPanel.add(cmButton);
        heightPanel.add(feetButton);

        cmButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {;
                  height1Label.setText("m");
                  height2Label.setText("cm ");
                }
            });

        feetButton.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                  height1Label.setText("ft");
                  height2Label.setText("inch");

                }
            });


        // Weight JComponents
        JLabel weightLabel = new JLabel("Weight:");
        JTextField weightField = new JTextField(4);
        JToggleButton kgButton = new JToggleButton("kg", true);
        JToggleButton lbButton = new JToggleButton("lbs");



        weightPanel.add(weightLabel);
        weightPanel.add(weightField);
        weightPanel.add(kgButton);
        weightPanel.add(lbButton);


        ButtonGroup weightGroup = new ButtonGroup();
        weightGroup.add(kgButton);
        weightGroup.add(lbButton);


        // tdee JComponents
        JLabel tdeeQuestionLabel = new JLabel("Calculate TDEE Also?");
        JRadioButton tdeeYes = new JRadioButton("Yes");
        JRadioButton tdeeNo = new JRadioButton("No", true);

        ButtonGroup tdeeButton = new ButtonGroup();
        tdeeButton.add(tdeeYes);
        tdeeButton.add(tdeeNo);

        tdeePanel.add(tdeeQuestionLabel);
        tdeePanel.add(tdeeYes);
        tdeePanel.add(tdeeNo);

        // activitylevel JComponents
        JLabel activityLevelLabel = new JLabel("Activity Level: ");
        JComboBox activityLevelBox = new JComboBox(activityLevels);
        activityLevelBox.setSelectedIndex(0);

        activityLevelPanel.add(activityLevelLabel);
        activityLevelPanel.add(activityLevelBox);
        activityLevelPanel.add(activityLevelHelp);
        activityLevelBox.setEnabled(false);

        activityLevelHelp
                .setToolTipText("<html><b>Sedentary:</b> little or no exercise, deskjob<<br /><b>Lightly Active:</b> little exercise/sports 1-3 days/week<br /><b>Moderately active:</b> moderate exercise/sports 3-5 days/week<br /><b>Very active:</b> hard exercise or sports 6-7 days/week<br /><b>Extra active:</b> hard daily exercise or sports & physical labor job </html>");
        ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);

        tdeeYes.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                  activityLevelBox.setEnabled(true);
                }
            });

        tdeeNo.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                  activityLevelBox.setEnabled(false);
                }
            });


        // tdee and BMR value components
        JLabel bmrLabel = new JLabel();
        JLabel tdeeLabel = new JLabel();
        JButton calculate = new JButton("Calculate");

        JPanel calcPane = new JPanel();
        calcPane.add(calculate);
        bmrTDEEValuesPanel.add(calcPane, BorderLayout.NORTH);
        bmrTDEEValuesPanel.add(bmrLabel, BorderLayout.CENTER);
        bmrTDEEValuesPanel.add(tdeeLabel, BorderLayout.SOUTH);

        // After the user presses the calculate button, error checking is done to ensure correct input is given (using regular expressions).
        // If error checking passes, user input is stored in variables which is then passed to the calcBMR method. 
        // The output from calcBMR is then passed into calcTDEE (if user selects this option)
        calculate.addActionListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                if (!ageField.getText().matches("[0-9]+")) {
                    JOptionPane.showMessageDialog(null, "Integer values only please.", "INCORRECT AGE INPUT", JOptionPane.WARNING_MESSAGE);
                } else if (!weightField.getText().matches("[0-9]+(.[0-9]+)?")) {
                    JOptionPane.showMessageDialog(null, "Decimal/integer values only please.", "INCORRECT WEIGHT INPUT", JOptionPane.WARNING_MESSAGE);
                } else if (cmButton.isSelected() && (!height1Field.getText().matches("[0-9]") || !height2Field.getText().matches("[0-9]([0-9]?)"))) {
                    JOptionPane.showMessageDialog(null, "Integer value for metre and cm field only please. Enter a value between 0 and 99 for cm field", "INCORRECT HEIGHT INPUT", JOptionPane.WARNING_MESSAGE);
                } else if (feetButton.isSelected() && (!height1Field.getText().matches("[0-9]") || !height2Field.getText().matches("[0-9]|10|11"))) {
                    JOptionPane.showMessageDialog(null, "Integer value for feet and inch field only please. Enter a value between 0-11 for inches field", "INCORRECT HEIGHT INPUT", JOptionPane.WARNING_MESSAGE);
                } else {
                    userAge = Integer.parseInt(ageField.getText());
                    // conversions done from lb to kg and feet/inches to cm below.
                    userWeight = kgButton.isSelected() ? Double.parseDouble(weightField.getText()) : Double.parseDouble(weightField.getText()) * 0.453592;;
                    userHeight = cmButton.isSelected() ? Double.parseDouble(height1Field.getText())*100 + Double.parseDouble(height2Field.getText()) : 
                        (Double.parseDouble(height1Field.getText())*12 + Double.parseDouble(height2Field.getText())) * 2.54;
                    userGender = genderMale.isSelected() ? "M" : "F";
                }


                userActivityLevel = tdeeYes.isSelected() ? activityMap.get(activityLevelBox.getSelectedItem()) : 1.0; // Looks up selected item from combo box, which is the KEY. Then looks up the value to this key from the map - this value is the TDEE multiplier.
                User user1 = new User(userAge, userGender, userHeight, userWeight, userActivityLevel); // An object of type Calculations needs to be created to access instance methods in Calculations class - objectReference.methodName(); syntax is being used
                bmrLabel.setText("<html><br /><font size=4>You have a <i><font color=#ce0000>BMR</font></i> of: " + "<font color=#59AF0E>" +  user1.getBMR() + "</font></html>");

                if (tdeeYes.isSelected()) {
                    tdeeLabel.setText("<html><br /><font size=4>You have a <i><font color=#ce0000>TDEE</font></i> of: " + "<font color=#59AF0E>" + user1.getTDEE() + "</font></html>");
                } else { 
                    tdeeLabel.setText(""); // Used to clear text if user chooses to calculate TDEE first time but chooses not to calculate second time and thereafter
                }


                User user2 = new User(20, "M", 140.0, 70.5, 1.725);
                System.out.println("###############   AGE   ###############");
                System.out.println(user1.getAge());
                System.out.println(user2.getAge());
                System.out.println("###############   WEIGHT   ###############");
                System.out.println(user1.getWeight());
                System.out.println(user2.getWeight());
                System.out.println("###############   HEIGHT   ###############");
                System.out.println(user1.getHeight());
                System.out.println(user2.getHeight());
                System.out.println("###############   GENDER   ###############");
                System.out.println(user1.getGender());
                System.out.println(user2.getGender());
                System.out.println("###############   MULT   ###############");
                System.out.println(user1.getActivityMultiplier());
                System.out.println(user2.getActivityMultiplier());
                System.out.println("###############   BMR   ###############");
                System.out.println(user1.getBMR());
                System.out.println(user2.getBMR());
                System.out.println("###############   TDEE   ###############");
                System.out.println(user1.getTDEE());
                System.out.println(user2.getTDEE());

            }
        });


        // Adding sub JPanels to main JPanel
        mainPanel.add(imgPanel);

        combinedGAHWpanel.add(agePanel, BorderLayout.NORTH); // Combine genderPanel and agePanel (which are both flowLayouts) into a
                                                             // single BorderLayout panel where agePanel is given the Northern spot and
                                                             // genderPanel is given the center spot in the panel
        weightHeightPanel.add(weightPanel, BorderLayout.NORTH); // Nested borderlayouts, the weightHeightPanel is another borderLayout which is nested
                                                                 // into the southern position of the combinedGAHW border layout.
        weightHeightPanel.add(heightPanel, BorderLayout.CENTER);
        weightHeightPanel.add(tdeeBMRPanel, BorderLayout.SOUTH);


        combinedGAHWpanel.add(genderPanel, BorderLayout.CENTER);
        combinedGAHWpanel.add(weightHeightPanel, BorderLayout.SOUTH);

        mainPanel.add(combinedGAHWpanel);

        // adding to tdeeBMRPanel
        tdeeBMRPanel.add(tdeePanel, BorderLayout.NORTH);
        tdeeBMRPanel.add(activityLevelPanel, BorderLayout.CENTER);
        tdeeBMRPanel.add(bmrTDEEValuesPanel, BorderLayout.SOUTH);

        // Adding main JPanel and menubar to JFrame
        setJMenuBar(menuBar);
        add(mainPanel);
    }

    public void initializeActivityLevelMap() {
        activityMap = new HashMap<String, Double>();
        activityMap.put("Sedentary", 1.2);
        activityMap.put("Lightly Active", 1.375);
        activityMap.put("Moderately Active", 1.55);
        activityMap.put("Very Active", 1.725);
        activityMap.put("Extra Active", 1.9);
    }

    public static void main(String[] args) {

        BmrMain gui = new BmrMain("BMR/TDEE Calculator"); 
        /*invokes the constructor above with the specified title parameter, the constructor creates the main frame with the setTitle(title) 
        line which calls the setTitle method from the JFrame superclass (as we used "extends JFrame" when creating this class)
        i.e, we create a bmrCalc object called gui using the non-default constructor (as we pass a title parameter, specifically "BMR/TDEE Calculator", 
        and this constructor also creates the main frame we are working in.
        */

        gui.setVisible(true);
        gui.setSize(330, 465);
        gui.setResizable(false);
        gui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);


    }
}

and the class that packs together user information and calculation methods:

public class User {

    private int age;
    private String gender; // todo: use an Enum
    private double height; // height stored in cm, weight in kg (so if user enters in feet/lbs, conversions are done to cm/kg and *THEN* passed through to constructor below)
    private double weight;
    private double activityMultiplier; // todo: use an Enum  (possibly)
    private int bmr;
    private int tdee;

    static final int MALEOFFSET = 5;  // Miffin St-Jeor equation is: (10 * weight (kg)) + (6.25 * height (cm)) + (5 * age) + OFFSET, where offset is 5 for males, -161 for females.
    static final int FEMALEOFFSET = -161;


    // This constructor is called everytime the program is run, used to create a User object to pack all user information together. 
    //This information comes from the UI which is coded in BMRMain
    public User(int age, String gender, double height, double weight, double activityMultiplier) {
        this.age = age;
        this.gender = gender;
        this.height = height;
        this.weight = weight;
        this.activityMultiplier = activityMultiplier;

        bmr = calcBMR();
        tdee = calcTDEE(bmr); // Calculates and stores tdee regardless of user selecting the option, but does not display if user did not select the option
    }

    /**
     * If user input is correct, this method will calculate the BMR value of the user given their input and measurement choices.
     * 
     * @param None
     * @return BMR Value
     */
    private final int calcBMR() {
        int offset = gender.equals("M") ? MALEOFFSET : FEMALEOFFSET;
        // This is the body of the calculations - different offset used depending on gender. Conversions to kg and cm done earlier so no conversions needed here.
        // The formula for male and female is similar - only the offset is different.
        return (int) (Math.round((10 * weight) + (6.25 * height) - (5 * age) + offset)); // This is the Miffin St-Jeor formula, calculations done in cm/kg
        }

    /**
     * If the user selects the TDEE option, this method will be executed after the calcBMR() method. 
     * A value from the calcBMR() method will be passed down to this method, and is multiplied
     * by the activity level parameter passed into this method.
     * 
     * @param bmr (output from calcBMR() method
     * @return TDEE Value
     */
    private final int calcTDEE(int bmr) {
        return (int) Math.round(bmr * activityMultiplier);
    }
    // calcBMR() and calcTDEE() can both be private as they are only called within this 
    // class (constructor), and we then use getBMR() and getTDEE() to access the returned values.



    public int getAge() {
        return age;
    }

    public String getGender() {
        return gender;
    }

    public double getHeight() {
        return height;
    }

    public double getWeight() {
        return weight;
    }

    public double getActivityMultiplier() {
        return activityMultiplier;
    }

    public int getBMR() {
        return bmr;
    }

    public int getTDEE() {
        return tdee;
    }

}
\$\endgroup\$
2
\$\begingroup\$
    JFrame mainFrame;

This variable is never used and can be deleted.

    JPanel mainPanel;

You don't need this to be an object field (sometimes called an instance field). It's enough for it to be a local variable. I.e.

        mainPanel = new JPanel();

could be

        JPanel mainPanel = new JPanel();

This is true of most of your object fields.

The advantage of keeping it to a local variable is that it reduces the scope and visibility. Note that if someone adds another class to the same package they would be able to see and change mainPanel with the original definition. This can get confusing, which is why we have scope and visibility limitations.

As a rule of thumb, you should try to limit the scope as narrowly as possible.

I would initialize the variable in the declaration unless there is some reason why it might need to be initialized to different values. E.g. different constructors or based on a method parameter. Note that if you are initializing a collection variable (e.g. a Map), you will have to add the elements separately in many cases.

    private  JTextField ageField;

This is a little different. Because you use it in the interior method, you have to do something to make it persist beyond the scope of a local variable. Making it an object field does this. Another alternative would be to make

        ageField = new JTextField(4);

into a final variable.

        final JTextField ageField = new JTextField(4);

If you do keep it as an object field, private is the correct visibility.

        bmrValuePanel = new JPanel();
        tdeeValuePanel = new JPanel();

These too are never used and can be removed.

        // Activity level map
        activityMap = new HashMap<String, Double>();
        activityMap.put("Sedentary", 1.2);
        activityMap.put("Lightly Active", 1.375);
        activityMap.put("Moderately Active", 1.55);
        activityMap.put("Very Active", 1.725);
        activityMap.put("Extra Active", 1.9);

Rather than using a comment to name this, consider creating a method

    private void initializeActivityValues() {
        activityValues = new HashMap<String, Double>();
        activityValues.put("Sedentary", 1.2);
        activityValues.put("Lightly Active", 1.375);
        activityValues.put("Moderately Active", 1.55);
        activityValues.put("Very Active", 1.725);
        activityValues.put("Extra Active", 1.9);
    }

This also has the benefit of making the original method shorter, particularly if you do the same thing with other sections.

I also changed the name. You may be able to do better. I didn't try to figure out what the numbers mean.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your suggestions, I will implement them. The only thing that confuses me is when you talk about declaring and intializing variables in a one liner - is there any rule to how and when this should be done? What is the difference between "object field" and a local variable? Also, do you suggest I put my initializeActivityValues in my User or BmrMain method? Thanks. \$\endgroup\$ – user82546 Sep 3 '15 at 21:37
  • \$\begingroup\$ Can't edit my previous posts for some reason. You say "Because you use it in the interior method.." - however, the ageField, heightField, genderButton etc are only used locally within the constructor, and the values are then extracted to variables (userAge, userGender etc). Can I not also declare ageField, heightField, genderButton etc as local variables within the constructor, and only leave userAge, userGender, userWeight, userHeight as global variables (again are object fields the same as global variables? - I've never heard of the phrase object fields). Thanks. \$\endgroup\$ – user82546 Sep 3 '15 at 22:29
  • \$\begingroup\$ I've implemented your suggestions. I'd appreciate if you could have a look. Regarding the initializeActivityValues map, I've implemented it but are we really making the original code shorter? There's an extra line for the method call and an extra line for the method declaration. Thanks. \$\endgroup\$ – user82546 Sep 3 '15 at 22:36
  • \$\begingroup\$ We aren't making the code shorter -- just the method. \$\endgroup\$ – mdfst13 Sep 4 '15 at 0:17
  • \$\begingroup\$ By method you mean the constructor right? To make the code more modular right? What do you think about the other things I mentioned? \$\endgroup\$ – user82546 Sep 4 '15 at 0:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy