5
\$\begingroup\$

Continuing to improve on my C skills.

This is a calculator program that runs in the terminal.

Given a segment of wire of a certain material, gauge, and length, calculates the total number of free electrons (electrons with high mobility), free charge density, and drift velocity at 1 amp.

Source also available in the repo https://github.com/BitCruncher0/FreeElectron

main.c

#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>

#include "defines.h"
#include "prompt.h"
#include "convert.h"
#include "geometry.h"
#include "material.h"
#include "wire.h"


double calc_drift_speed(double current, double carrier_density, double area)
{
    return current / (carrier_density * ELECTRONIC_CHARGE * area);
}


int main(void)
{
#ifdef RUN_REAL_PROGRAM
    /* Todo
       1. Prompt for radius instead of AWG */

    /* PROGRAM FLOW

       1. Prompt for wire material
       2. Prompt for AWG
       3. Prompt for wire length
       4. Give statistics

       To quit, press enter at prompts */

    while(1) {
        char request_program_quit;

        const struct Material *material = prompt_material(&request_program_quit);
        if(request_program_quit) return 0;

        int awg = prompt_awg(&request_program_quit);
        if(request_program_quit) return 0;

        double length = prompt_length(&request_program_quit);
        if(request_program_quit) return 0;

        double diameter = awg_diameter(awg);
        double radius = diameter / 2.0;
        double area = area_circle(radius);
        double volume = volume_cylinder(radius, length);
        double volume_in_cm3 = mm3_to_cm3(volume);

        // mass = density x volume
        double mass_in_grams =
            material->VOLUMETRIC_DENSITY * volume_in_cm3;

        // moles = mass / molar mass
        double moles =
            mass_in_grams / material->ATOMIC_MASS;

        double atoms =
            AVOGADROS_NUMBER * moles;

        double free_electrons =
            material->FREE_ELECTRONS_PER_ATOM * atoms;

        double free_charge =
            free_electrons * -ELECTRONIC_CHARGE;

        double carrier_density =
            free_electrons / volume;

        double drift_speed =
            calc_drift_speed(1.0, carrier_density, area);

        fputc('\n', stdout);

        printf("%i AWG %s\n", awg, material->NAME);
        printf(
            "radius: %.*F mm\tdia: %.*F mm\tarea: %.*F mm^2\n",
            PRECISION,
            radius,
            PRECISION,
            diameter,
            PRECISION,
            area);
        printf("volume: %.*F mm^3\n", PRECISION, volume);
        printf("mass: %.*F g\n", PRECISION, mass_in_grams);
        printf("moles: %.*F mol\n", 2 * PRECISION, moles);
        printf("atoms: %.*E atom\n", PRECISION, atoms);
        printf("free elec: %.*E elec\n", PRECISION, free_electrons);
        printf("free charge: %.*E C\n", 2 * PRECISION, free_charge);
        printf("carrier density: %.*E elec/mm^3\n", PRECISION, carrier_density);
        printf("sdrift @ 1 A: %.*E mm/s\n", PRECISION, drift_speed);

        fputc('\n', stdout);
    }
#endif
}

defines.h

#pragma once

#define RUN_REAL_PROGRAM

#define PRECISION           3
#define PI                  3.14159
#define AVOGADROS_NUMBER    6.022 * pow(10, 23)
#define ELECTRONIC_CHARGE   1.602 * pow(10, -19)
#define INPUT_BUFFER_SIZE   255

prompt.h

#pragma once

/* Prints out a message and prompts the user for input.
   The zero-terminated input is stored in the buffer; the newline is stripped.

   Characters are read up until the first newline or until
   num_chars - 1 are read, whichever comes first.

   Returns a pointer to the buffer. */
char *prompt(const char *msg, char *buffer, size_t num_chars);

/* Like prompt(), except a quit string is given.

   If the input is the same as quit_str, returns 1.
   Returns 0 otherwise. */
char prompt_quit(
    const char *msg, char *buffer, size_t num_chars, const char *quit_str);

/* Continually prompts the user for a long-formatted string until one is given
   or until the input is equal to quit_str.

   If the input was equal to quit_str, then the flag pointed to by req is set
   and returns 0.

   Otherwise, the flag pointed to by req is cleared
   and returns the long value. */
long prompt_long(const char *msg, const char *quit_str, char *req);
double prompt_double(const char *msg, const char *quit_str, char *req);

/* Strips str of the newline character at the end.

   Returns 0 on successful removal or 1 if no newline
   character is found.

   str points to a zero-terminated string that contains at most one
   newline character. */
char removeNewline(char *str);

prompt.c

#include <stdio.h>
#include <string.h>

#include "defines.h"
#include "prompt.h"
#include "convert.h"


char *prompt(const char *msg, char *buffer, size_t num_chars)
{
    fputs(msg, stdout);
    fgets(buffer, num_chars, stdin);
    removeNewline(buffer);
    return buffer;
}


char prompt_quit(
    const char *msg, char *buffer, size_t num_chars, const char *quit_str)
{
    if(strcmp(prompt(msg, buffer, num_chars), quit_str)) return 0;
    else return 1;
}


long prompt_long(const char *msg, const char *quit_str, char *request_quit)
{
    while(1) {
        char buf[INPUT_BUFFER_SIZE + 1];
        if(prompt_quit(msg, buf, INPUT_BUFFER_SIZE + 1, quit_str)) {
            *request_quit = 1;
            return 0;
        }

        // Attempt to convert string to numerical
        char conversion_error;
        long result = cstr_to_long(buf, &conversion_error);
        if(conversion_error) {
            fputs("Invalid input\n\n", stdout);
            continue;
        }

        *request_quit = 0;
        return result;
    }
}


double prompt_double(const char *msg, const char *quit_str, char *request_quit)
{
    while(1) {
        char buf[INPUT_BUFFER_SIZE + 1];
        if(prompt_quit(msg, buf, INPUT_BUFFER_SIZE + 1, quit_str)) {
            *request_quit = 1;
            return 0;
        }

        // Attempt to convert string to numerical
        char conversion_error;
        double result = cstr_to_double(buf, &conversion_error);
        if(conversion_error) {
            fputs("Invalid input\n\n", stdout);
            continue;
        }

        *request_quit = 0;
        return result;
    }
}


char removeNewline(char *str)
{
    int i;
    for(i = 0; str[i] != 0; i++) {
        if(str[i] == '\n') {
            str[i] = 0;
            return 0;
        }
    }
    return 1;
}

convert.h

#pragma once

/* Converts zero-terminated string to long int.
   If no conversion could be made, flag at conversion_error is set and
   returns 0.
   Otherwise, flag at conversion_error is cleared and returns the value. */
long cstr_to_long(const char *, char *conversion_error);
double cstr_to_double(const char *, char *conversion_error);

convert.c

#include <stdlib.h>

long cstr_to_long(const char *str, char *conversion_err)
{
    char *end;
    long result = strtol(str, &end, 10);
    *conversion_err = ((*end) == 0) ? 0 : 1;
    return result;
}

double cstr_to_double(const char *str, char *conversion_err)
{
    char *end;
    double result = strtod(str, &end);
    *conversion_err = ((*end) == 0) ? 0 : 1;
    return result;
}

geometry.h

#pragma once

double area_circle(double radius);
double volume_cylinder(double radius, double length);
double mm3_to_cm3(double mm3);

geometry.c

#include <math.h>
#include "defines.h"

double area_circle(double radius)
{
    return PI * pow(radius, 2);
}

double volume_cylinder(double radius, double length)
{
    return length * area_circle(radius);
}

double mm3_to_cm3(double mm3)
{
    return mm3 / 1000.0;
}

material.h

#pragma once

struct Material {
    const char *NAME;
    const double ATOMIC_MASS;           //g / mol
    const int FREE_ELECTRONS_PER_ATOM;  //#
    const double VOLUMETRIC_DENSITY;    //g / cm3
};

void print_materials_list(void);
const struct Material *str_to_material(const char *str);
const struct Material *prompt_material(char *quit);

material.c

#include <stdio.h>
#include <string.h>
#include "defines.h"
#include "material.h"
#include "prompt.h"

const struct Material COPPER = {
    "copper",
    63.546,
    1,
    8.96 };

const struct Material ALUMINIUM = {
    "aluminium",
    26.982,
    3,
    2.7 };

const struct Material GOLD = {
    "gold",
    196.97,
    1,
    19.3 };

const struct Material NULL_MATERIAL = {
    "null_material",
    0,
    0,
    0 };

void print_materials_list(void)
{
    fputs("Materials list\n", stdout);
    fputs("1. copper\n", stdout);
    fputs("2. aluminium\n", stdout);
    fputs("3. gold", stdout);
}

const struct Material *str_to_material(const char *str)
{
    if(!strcmp(str, "copper")) return &COPPER;
    else if(!strcmp(str, "aluminium")) return &ALUMINIUM;
    else if(!strcmp(str, "gold")) return &GOLD;
    else return &NULL_MATERIAL;
}

const struct Material *prompt_material(char *quit)
{
    while(1) {
        print_materials_list();
        fputs("\n\n", stdout);

        char buf[INPUT_BUFFER_SIZE + 1];
        if(prompt_quit("Material? ", buf, INPUT_BUFFER_SIZE + 1, "")) {
            *quit = 1;
            return &NULL_MATERIAL;
        }
        else {
            const struct Material *p_mat = str_to_material(buf);
            if(p_mat == &NULL_MATERIAL) {
                fputs("Not a valid material\n\n", stdout);
            }
            else {
                *quit = 0;
                return p_mat;
            }
        }
    }
}

wire.h

#pragma once

/* Returns diameter (in millimeters) of given AWG */
double awg_diameter(int AWG);

/* Continually prompts the user for a valid wire gauge number until one is
   entered or until an empty string is entered.

   If an empty string is entered, the flag pointed to by quit is set and
   returns 0, otherwise the flag at quit is cleared and returns the wire
   gauge. */
int prompt_awg(char *quit);
double prompt_length(char *quit);

void TEST_print_awg_diameters(void);
void TEST_print_awg_areas(void);

wire.c

#include <stdio.h>
#include <math.h>
#include "prompt.h"
#include "geometry.h"

double awg_diameter(int awg)
{
    return 0.127 * pow(92, (36.0 - awg) / 39);
}

int prompt_awg(char *quit)
{
    while(1) {
        long awg = prompt_long("AWG [0 - 36]? ", "", quit);
        if(*quit) return 0;

        // 0 <= AWG <= 36
        else if((awg < 0) || (awg > 36)) fputs("Not a valid AWG\n\n", stdout);
        else {
            *quit = 0;
            return (int)awg;
        }
    }
}

double prompt_length(char *quit)
{
    while(1) {
        double length = prompt_double("Wire length (mm) (0 - inf)? ", "", quit);
        if(*quit) return 0;

        // 0 < Wire length
        else if(length <= 0) fputs("Not a valid wire length\n\n", stdout);
        else {
            *quit = 0;
            return length;
        }
    }
}

void TEST_print_awg_diameters(void)
{
    int awg;
    for(awg = 0; awg <= 36; awg++) {
        printf("%d: %.*F\n", awg, 3, awg_diameter(awg));
    }
    fputc('\n', stdout);
}

void TEST_print_awg_areas(void)
{
    int awg;
    for(awg = 0; awg <= 36; awg++) {
        printf("%i: %.*F\n", awg, 3, area_circle(awg_diameter(awg) / 2.0));
    }
    fputc('\n', stdout);
}

\$\endgroup\$
2
\$\begingroup\$

Constants weaknesses

OP uses a reduced precision version of pi. I recommend a better one. Code like 6.022 * pow(10, 23) should use a () to insure it does not get an out-of-order evaluation as with !AVOGADROS_NUMBER. Using * pow(10, 23) can incurring a not-the-best encoding of 6.022e10-23 due to 2 roundings of OP's multiplication and function call. Use a good referenced value N, e. I'd add a comment to the ref link.

For physical constants with units, good form to append the units to the constant.

// Instead of ...
#define PI                  3.14159
#define AVOGADROS_NUMBER    6.022 * pow(10, 23)
#define ELECTRONIC_CHARGE   1.602 * pow(10, -19)

// Use 
// en.wikipedia.org/wiki/Pi
#define PI                  3.1415926535897932384626433
// en.wikipedia.org/wiki/Avogadro_constant
#define AVOGADROS_NUMBER    6.02214076e23
// en.wikipedia.org/wiki/Elementary_charge
#define ELECTRONIC_CHARGE   1.602176634e−19  /* coulombs */

Various magic numbers like 63.546 for copper deserve a reference and units.

Good to document units in code and .h files.

// return 0.127 * pow(92, (36.0 - awg) / 39);
return 0.127 /* mm */ * pow(92, (36.0 - awg) / 39);

Does not handle end-of-file

When fgets() return NULL due to end-of-file or input error, prompt() returns buffer in an unknown state. Calling code has no clue end-of-file occurred.

// Improved
char *prompt(const char *msg, char *buffer, size_t num_chars) {
  // Allow calling code to skip the prompt and not flush.
  if (msg) {
    fputs(msg, stdout);
    fflush(stdout); // Insure output seen before input. 
  }
  if (fgets(buffer, num_chars, stdin) == NULL) {
    if (num_chars > 0) {
      buffer[0] = '\0';  // Insure buffer content is always defined
    }
    return NULL;
  }
  removeNewline(buffer);
  return buffer;
}

Improvement better handling of long lines than leave the extra in stdin. IMO, it is an error input and the entire line should be read and tossed.

Corner conversion failings

cstr_to_long() and cstr_to_double() both are fooled when buffer[0] == 0. (It is possible to enter '\0' as the first character read.)

cstr_to_long() fails to detect/report overflow.

// Candidate improvement    
long cstr_to_long(const char *str, char *conversion_err) {
  char *end;
  errno = 0;
  long result = strtol(str, &end, 10);
  if (str == end) {
    *conversion_err = 1; // No conversion
  } else if (errno == ERANGE) {
    *conversion_err = 1; // Overflow
  } else if (errno) {
    *conversion_err = 1; // Implementation specific error
  } else if (*end) {
    *conversion_err = 1; // Junk after the number
  } else {
    *conversion_err = 0;
  }
  return result;
}

Missing *.h

A *.c file should include its *.h first to ascertain correctness and test the *.h file's ability to stand on its own.

Edit to address comment

Consider prompt.h with its use of size_t. prompt.h lacks an include like #include <stdio.h> and so if some user's file included prompt.h first, compilation would fail. prompt.c does not fail OP's code because it includes various <*.h> files first. By having prompt.c include prompt.h first, such missing includes are detected. A good .h file includes all its needed *.h files and no more. A good .h does not depend on users of that file to include other .h files.

Example: prompt.c

#include "prompt.h"  // Include companion .h file first
#include <stdio.h>
#include <string.h>

#include "defines.h"
//#include "prompt.h" // Not here
#include "convert.h"

Naming convention

There is none.

awg_diameter(), prompt_awg(), prompt_length(), TEST_print_awg_diameters(), TEST_print_awg_areas() all originate in wire.h - Hmmm.

Consider instead wire function named wire_... from wire.h, Test functions from TEST.h, etc.

Case less compare

I'd tolerate case-less compares and maybe periodic table abbreviation. stricmp() and other though are implementation defined - perhaps make your own wrapper.

if(!stricmp(str, "copper") || !strcmp(str, "Cu")) 

char vs. bool

For functions returning 0 or 1, return an int or better yet a bool. char is a strange choice.

\$\endgroup\$
2
  • \$\begingroup\$ I know this is late, but could you expound on the point of the missing *.h, specifically the part where you mention the header's "ability to stand on its own"? Thanks. \$\endgroup\$
    – Mode77
    Jan 13 at 4:25
  • \$\begingroup\$ @Mode77 Answer amended with detail. \$\endgroup\$ Jan 13 at 12:16
2
\$\begingroup\$

In addition to the other reviews, some misc comments:

  • The switch RUN_REAL_PROGRAM is weird, doesn't seem like something that should be part of the final program - remove it.

  • In general, avoid non-standard C if there is no good reason for it. That is, replace for example non-standard #pragma once with standard C #ifndef SOMETHING_H #define SOMETHING_H ... #endif.

  • Don't create some generic header called "defines.h". Instead place those defines in the module where they actually belong. AVOGADROS_NUMBER should probably be in material.h (?), INPUT_BUFFER_SIZE should be in prompt.h and so on.

  • I prefer to place all includes in the .h file rather than the .c file since the former is the interface to the user. The user needs to know all file dependencies of a .h + .c pair and shouldn't need to go digging through the .c file (which might not even be available) to find them.

  • For very long function declarations/definitions, consider using new line style:

    char prompt_quit(const char *  msg, 
                     char *        buffer, 
                     size_t        num_chars, 
                     const char *  quit_str);
    
  • Avoid "for ever" loops when they aren't called for. Also, the presence of continue in a C program is almost always an indication of a poorly designed loop. So instead of the somewhat obscure: while(1) { ... if(conversion_error) { ... continue; } ... return result; }, write something like this instead:

      char conversion_error = /* non-zero value */;
    
      while(conversion_error)
      {
        ...
        long result = cstr_to_long(buf, &conversion_error);
        if(conversion_error) {
          fputs("Invalid input\n\n", stdout);
        }
      }
    
      *request_quit = 0;
      return result;
    } // end of function
    
  • If conversion_error is only either 1 or 0 then it should be bool not char. Overall, it is common convention to reserve the return type of functions for the error handling, especially when supported more detailed errors (through an enum type etc).

  • Your list of different materials should be implemented as an array of struct instead.

  • All constants like 36.0 and 39 have a type in C, in this case double and int respectively. Always avoid mixing floating point and fixed point in the same expression, since that's an easy way to get subtle bugs. For example 123 / 456 + 789.0 first gets the 123 / 456 calculated on int then implicitly promoted to double afterwards.

    So for example change return 0.127 * pow(92, (36.0 - awg) / 39); to return 0.127 * pow(92.0, (36.0 - awg) / 39.0);

  • Is AWG 0 really a thing?

\$\endgroup\$
5
  • 1
    \$\begingroup\$ "Is AWG 0 really a thing?" Yes. \$\endgroup\$
    – Mode77
    Jan 14 at 1:32
  • \$\begingroup\$ "The user needs to know all file dependencies of a .h + .c pair" contradicts my rule of thumb. The header should include everything required for the header (a good test of that is to make it the first include of the translation unit), but it shouldn't include libraries required only to implement its definitions (and which are subject to change as the implementation is refined). Do you disagree with that principle? \$\endgroup\$ Jan 14 at 7:53
  • \$\begingroup\$ @TobySpeight Say that I deliver the application programmer a library where you are only supposed to use the header. I forgot to link some internal lib to my project or to document that you need a certain lib. Then when you try to link my lib, you get "mysterious linker error 1 in foo.c, missing mysterious function". At which point you have no idea what "mysterious function" is, why it is missing, where it should be found or what caused the problem. Had I however listed all relevant header in the h file that you have access to, you would be able to trouble-shoot the problem as missing headers. \$\endgroup\$
    – Lundin
    Jan 14 at 10:06
  • \$\begingroup\$ And I bet every C veteran out there can relate to situations where they have tried to integrate a 3rd party lib in their program and got shot down by such nondescript linker errors. It doesn't happen once or twice through your career, it happens some dozens of times. \$\endgroup\$
    – Lundin
    Jan 14 at 10:08
  • \$\begingroup\$ I'd argue that you should then fix the header comments for the library to document the link-time requirements, rather than expect the library user to inspect your source code. TBH, I've always found the linker errors more useful for determining missing libraries than any header includes. \$\endgroup\$ Jan 14 at 10:44

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.