Tuesday, September 22, 2026

IMPLEMENTING A SCIENTIFIC CALCULATOR FROM SCRATCH


 

INTRODUCTION

Building a scientific calculator from the ground up is an excellent exercise in understanding both mathematical algorithms and software design principles. While we assume the availability of basic arithmetic operations such as addition, subtraction, multiplication, division, modulo, and percentile calculations, every other mathematical function must be implemented manually. This tutorial will guide you through the process of creating a fully functional scientific calculator that can handle trigonometric functions, logarithms, exponentials, roots, and more.

The challenge lies not just in implementing these functions, but in doing so with sufficient accuracy, efficiency, and numerical stability. We will explore the mathematical foundations behind each function, discuss various implementation strategies, and build a complete calculator that adheres to clean code principles.

FOUNDATIONAL CONCEPTS

Before diving into specific implementations, we need to understand several foundational concepts that will guide our design decisions.

Numerical Precision and Floating Point Arithmetic

When working with mathematical computations, we must be aware of the limitations of floating point arithmetic. Computers represent real numbers using a finite number of bits, which means that not all real numbers can be represented exactly. This leads to rounding errors that can accumulate through successive calculations.

For our calculator, we will work with double precision floating point numbers, which provide approximately 15 to 17 decimal digits of precision. This is sufficient for most scientific calculations, but we must be mindful of operations that can lead to catastrophic cancellation or loss of significance.

Series Expansions and Iterative Methods

Many mathematical functions can be computed using series expansions. For example, the exponential function can be expressed as an infinite Taylor series. In practice, we truncate these series after a finite number of terms, choosing enough terms to achieve the desired accuracy.

Iterative methods provide another approach for computing functions. These methods start with an initial guess and repeatedly refine it until the result converges to the true value within a specified tolerance. Newton's method is a classic example of an iterative approach.

Range Reduction

Computing functions directly for all possible input values can be inefficient or numerically unstable. Range reduction is a technique where we transform the input to a smaller, more manageable range, compute the function in that range, and then transform the result back to the original domain.

For instance, when computing sine of a large angle, we can reduce it to an equivalent angle between zero and two pi by exploiting the periodicity of the sine function. This allows us to use a more accurate and efficient algorithm for the reduced range.

IMPLEMENTING THE EXPONENTIAL FUNCTION

The exponential function e raised to the power x is fundamental to many other calculations. We will implement it using a combination of range reduction and Taylor series expansion.

Range Reduction Strategy

For very large or very small values of x, direct computation using a Taylor series would require many terms. Instead, we use the property that e to the power x equals e to the power of the integer part times e to the power of the fractional part. We can compute e to the power of an integer using repeated squaring, and use the Taylor series for the fractional part.

Additionally, we can further reduce the range by using the identity e to the power x equals the square of e to the power of x divided by two. By repeatedly halving the input, we can bring it into a range where the Taylor series converges rapidly.

Here is the core exponential function implementation:

double exp_custom(double x) {
    // Handle special cases
    if (x == 0.0) return 1.0;
    if (x < -700.0) return 0.0;  // Underflow
    if (x > 700.0) return 1.0 / 0.0;  // Overflow to infinity
    
    // Range reduction: separate integer and fractional parts
    int n = (int)x;
    double f = x - n;
    
    // Further reduce f to [-0.5, 0.5]
    if (f > 0.5) {
        n += 1;
        f -= 1.0;
    } else if (f < -0.5) {
        n -= 1;
        f += 1.0;
    }
    
    // Compute exp(f) using Taylor series
    double result = 1.0;
    double term = 1.0;
    for (int i = 1; i <= 20; i++) {
        term = term * f / i;
        result = result + term;
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    // Compute exp(n) using repeated squaring
    double exp_n = 1.0;
    double base = 2.718281828459045;  // e
    int abs_n = n < 0 ? -n : n;
    
    while (abs_n > 0) {
        if (abs_n % 2 == 1) {
            exp_n = exp_n * base;
        }
        base = base * base;
        abs_n = abs_n / 2;
    }
    
    if (n < 0) {
        exp_n = 1.0 / exp_n;
    }
    
    return result * exp_n;
}

The function first handles special cases where x is zero, very large, or very small. For the general case, it separates x into integer and fractional components. The fractional part is further reduced to the range negative one half to positive one half, which ensures rapid convergence of the Taylor series.

The Taylor series for the exponential function is one plus x plus x squared over two factorial plus x cubed over three factorial and so on. We compute this series iteratively, accumulating terms until they become negligibly small. The loop terminates either after twenty iterations or when the term becomes smaller than machine epsilon.

For the integer part, we use the fact that e to the power n equals e multiplied by itself n times. Rather than performing n multiplications, we use the repeated squaring technique, which reduces the number of operations to logarithmic in n.

IMPLEMENTING THE NATURAL LOGARITHM

The natural logarithm is the inverse of the exponential function. Computing logarithms accurately requires careful attention to numerical stability, especially for arguments close to one.

Using Range Reduction

We exploit the property that the logarithm of a product equals the sum of logarithms. Any positive number can be expressed as a power of two times a mantissa in the range one to two. We can compute the logarithm of the power of two exactly, and use a series expansion for the mantissa.

For the mantissa in the range one to two, we further reduce it to a value close to one by using the identity that the logarithm of x equals the logarithm of x divided by the square root of two plus the logarithm of the square root of two. By repeatedly applying this transformation, we bring the argument very close to one, where a simple series converges rapidly.

Here is the implementation:

double ln_custom(double x) {
    // Handle special cases
    if (x <= 0.0) return -1.0 / 0.0;  // Undefined for non-positive
    if (x == 1.0) return 0.0;
    
    // Extract exponent and mantissa
    int exponent = 0;
    while (x >= 2.0) {
        x = x / 2.0;
        exponent = exponent + 1;
    }
    while (x < 1.0) {
        x = x * 2.0;
        exponent = exponent - 1;
    }
    
    // Now x is in [1, 2), reduce further to near 1
    double sqrt2 = 1.414213562373095;
    double ln_sqrt2 = 0.346573590279973;
    double adjustment = 0.0;
    
    while (x > 1.2) {
        x = x / sqrt2;
        adjustment = adjustment + ln_sqrt2;
    }
    while (x < 0.9) {
        x = x * sqrt2;
        adjustment = adjustment - ln_sqrt2;
    }
    
    // Use series expansion for ln(1 + u) where u = x - 1
    double u = x - 1.0;
    double result = 0.0;
    double term = u;
    
    for (int i = 1; i <= 50; i++) {
        result = result + term / i;
        term = term * (-u);
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    // Add back the contributions from range reduction
    double ln2 = 0.693147180559945;
    return result + adjustment + exponent * ln2;
}

The function begins by handling edge cases such as non-positive arguments and the argument equal to one. For the general case, it first normalizes the input to the range one to two by extracting powers of two. The number of divisions or multiplications by two gives us the exponent component.

Next, we further reduce the mantissa to be close to one by repeatedly dividing or multiplying by the square root of two. This brings the argument into a range where the Taylor series for the logarithm of one plus u converges quickly.

The series expansion used is u minus u squared over two plus u cubed over three minus u fourth over four and so on, where u equals x minus one. This alternating series converges for u in the range negative one to one. We accumulate terms until they become negligible.

Finally, we add back the logarithm contributions from all the range reduction steps. The logarithm of two and the logarithm of the square root of two are precomputed constants.

IMPLEMENTING POWER FUNCTIONS

Computing x raised to the power y for arbitrary real numbers x and y requires combining our exponential and logarithm functions. The key identity is that x to the power y equals e to the power of y times the natural logarithm of x.

Handling Special Cases

Power functions have many special cases that must be handled carefully. When y is an integer, we can use repeated multiplication. When x is negative and y is not an integer, the result may be complex, which we will indicate as an error. When x is zero, the result depends on the sign of y.

Here is the implementation:

double power_custom(double x, double y) {
    // Handle special cases
    if (y == 0.0) return 1.0;
    if (x == 0.0) {
        if (y > 0.0) return 0.0;
        return 1.0 / 0.0;  // Infinity
    }
    if (x == 1.0) return 1.0;
    
    // Check if y is an integer
    int y_int = (int)y;
    if (y == (double)y_int) {
        // Use repeated multiplication for integer powers
        double result = 1.0;
        int abs_y = y_int < 0 ? -y_int : y_int;
        double base = x;
        
        while (abs_y > 0) {
            if (abs_y % 2 == 1) {
                result = result * base;
            }
            base = base * base;
            abs_y = abs_y / 2;
        }
        
        if (y_int < 0) {
            result = 1.0 / result;
        }
        return result;
    }
    
    // For non-integer powers, x must be positive
    if (x < 0.0) {
        return 0.0 / 0.0;  // NaN
    }
    
    // Use x^y = exp(y * ln(x))
    return exp_custom(y * ln_custom(x));
}

The function first checks for several special cases where the result can be determined immediately. When y is zero, any non-zero x raised to the power zero is one. When x is zero, the result is zero for positive y and infinity for negative y.

For integer exponents, we use the repeated squaring algorithm, which is more efficient and accurate than using logarithms and exponentials. This algorithm works by repeatedly squaring the base and selectively multiplying into the result based on the binary representation of the exponent.

For non-integer exponents, we require x to be positive to avoid complex results. We then use the fundamental identity that x to the power y equals e to the power of y times the natural logarithm of x. This reduces the problem to our previously implemented exponential and logarithm functions.

IMPLEMENTING SQUARE ROOT

The square root function is a special case of the power function, but it is so commonly used that it deserves its own optimized implementation. We will use Newton's method, which provides quadratic convergence.

Newton's Method for Square Root

Newton's method for finding the square root of a number a starts with an initial guess x zero and iteratively refines it using the formula x next equals one half times the quantity x plus a divided by x. This formula comes from applying Newton's method to the equation x squared minus a equals zero.

The method converges very rapidly. Each iteration approximately doubles the number of correct digits. Typically, only five or six iterations are needed to achieve full double precision accuracy.

Here is the implementation:

double sqrt_custom(double x) {
    // Handle special cases
    if (x < 0.0) return 0.0 / 0.0;  // NaN
    if (x == 0.0) return 0.0;
    if (x == 1.0) return 1.0;
    
    // Initial guess using bit manipulation for fast approximation
    double guess = x / 2.0;
    if (x > 1.0) {
        guess = x / 2.0;
    } else {
        guess = x;
    }
    
    // Newton's method iteration
    for (int i = 0; i < 10; i++) {
        double next_guess = 0.5 * (guess + x / guess);
        if (next_guess == guess) break;  // Converged
        guess = next_guess;
    }
    
    return guess;
}

The function begins by handling edge cases. The square root of a negative number is undefined in the real number system, so we return not a number. Zero and one are returned immediately as they are their own square roots.

For the general case, we need a reasonable initial guess. A simple choice is to use x divided by two for x greater than one, and x itself for x less than one. More sophisticated initial guesses could be obtained using bit manipulation, but our simple approach works well.

The iteration loop applies Newton's formula repeatedly. We check for convergence by comparing successive guesses. When they are equal within machine precision, we have converged and can terminate early. The loop is capped at ten iterations, which is more than sufficient for double precision.

IMPLEMENTING TRIGONOMETRIC FUNCTIONS

Trigonometric functions are essential for any scientific calculator. We will implement sine, cosine, and tangent, from which all other trigonometric functions can be derived.

Sine and Cosine Using Taylor Series

The sine and cosine functions can be computed using their Taylor series expansions. However, direct application of these series for large arguments would require many terms. We use range reduction to bring the argument into the range zero to pi over two.

The key properties we exploit are the periodicity of sine and cosine with period two pi, and their symmetries. For any angle, we can find an equivalent angle in the first quadrant and adjust the sign of the result accordingly.

Here is the sine implementation:

double sin_custom(double x) {
    // Define pi
    double pi = 3.141592653589793;
    
    // Reduce to [0, 2*pi)
    while (x >= 2.0 * pi) {
        x = x - 2.0 * pi;
    }
    while (x < 0.0) {
        x = x + 2.0 * pi;
    }
    
    // Reduce to [0, pi/2] using symmetries
    int sign = 1;
    if (x > pi) {
        x = x - pi;
        sign = -sign;
    }
    if (x > pi / 2.0) {
        x = pi - x;
    }
    
    // Taylor series for sin(x)
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    
    for (int i = 1; i <= 20; i = i + 2) {
        result = result + term;
        term = term * (-x_squared) / ((i + 1) * (i + 2));
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

The function first reduces the argument to the range zero to two pi by adding or subtracting multiples of two pi. This exploits the periodicity of the sine function.

Next, we use the symmetry properties of sine to further reduce the argument to the range zero to pi over two. If the angle is in the range pi to two pi, sine is negative, so we subtract pi and negate the sign. If the angle is in the range pi over two to pi, we use the identity that sine of x equals sine of pi minus x.

With the argument now in the range zero to pi over two, we apply the Taylor series for sine. The series is x minus x cubed over three factorial plus x to the fifth over five factorial minus x to the seventh over seven factorial and so on. We compute this efficiently by maintaining the current term and updating it using the recurrence relation.

The cosine function is implemented similarly:

double cos_custom(double x) {
    // Define pi
    double pi = 3.141592653589793;
    
    // Reduce to [0, 2*pi)
    while (x >= 2.0 * pi) {
        x = x - 2.0 * pi;
    }
    while (x < 0.0) {
        x = x + 2.0 * pi;
    }
    
    // Reduce to [0, pi/2] using symmetries
    int sign = 1;
    if (x > pi) {
        x = x - pi;
        sign = -sign;
    }
    if (x > pi / 2.0) {
        x = pi - x;
        sign = -sign;
    }
    
    // Taylor series for cos(x)
    double result = 0.0;
    double term = 1.0;
    double x_squared = x * x;
    
    for (int i = 0; i <= 20; i = i + 2) {
        result = result + term;
        term = term * (-x_squared) / ((i + 1) * (i + 2));
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

The cosine implementation follows the same pattern as sine, with appropriate adjustments for the different symmetry properties of cosine. The Taylor series for cosine is one minus x squared over two factorial plus x to the fourth over four factorial and so on.

The tangent function is simply the ratio of sine to cosine:

double tan_custom(double x) {
    double cos_x = cos_custom(x);
    if (cos_x == 0.0) {
        return 1.0 / 0.0;  // Infinity
    }
    return sin_custom(x) / cos_x;
}

We check if the cosine is zero to avoid division by zero. When cosine is zero, tangent is undefined, which we represent as infinity.

IMPLEMENTING INVERSE TRIGONOMETRIC FUNCTIONS

Inverse trigonometric functions allow us to find angles given trigonometric ratios. We will implement arcsine, arccosine, and arctangent.

Arcsine Using Series and Iteration

For small arguments, arcsine can be computed using a Taylor series. For larger arguments, we can use the identity that arcsine of x equals pi over two minus arcsine of the square root of one minus x squared for x close to one, or use Newton's method.

Here is the arcsine implementation:

double asin_custom(double x) {
    // Handle special cases
    if (x < -1.0 || x > 1.0) return 0.0 / 0.0;  // NaN
    if (x == 0.0) return 0.0;
    if (x == 1.0) return 3.141592653589793 / 2.0;
    if (x == -1.0) return -3.141592653589793 / 2.0;
    
    // For |x| > 0.7, use identity asin(x) = pi/2 - asin(sqrt(1-x^2))
    if (x > 0.7) {
        double pi_over_2 = 3.141592653589793 / 2.0;
        return pi_over_2 - asin_custom(sqrt_custom(1.0 - x * x));
    }
    if (x < -0.7) {
        double pi_over_2 = 3.141592653589793 / 2.0;
        return -pi_over_2 + asin_custom(sqrt_custom(1.0 - x * x));
    }
    
    // Taylor series for asin(x)
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    double numerator = x;
    double denominator = 1.0;
    
    for (int n = 0; n < 30; n++) {
        result = result + term;
        numerator = numerator * x_squared * (2 * n + 1) * (2 * n + 1);
        denominator = denominator * (2 * n + 2) * (2 * n + 3);
        term = numerator / denominator;
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return result;
}

The function handles the domain restriction that arcsine is only defined for arguments in the range negative one to one. For arguments near plus or minus one, we use the identity relating arcsine to the complementary angle to avoid numerical issues with the series expansion.

For arguments in the range negative 0.7 to 0.7, we use the Taylor series expansion. The series for arcsine is more complex than those for exponential or trigonometric functions, involving products of odd numbers in both numerator and denominator.

The arccosine function can be implemented using the identity that arccosine of x equals pi over two minus arcsine of x:

double acos_custom(double x) {
    double pi_over_2 = 3.141592653589793 / 2.0;
    return pi_over_2 - asin_custom(x);
}

For arctangent, we use a series expansion combined with range reduction:

double atan_custom(double x) {
    // Handle special cases
    if (x == 0.0) return 0.0;
    
    // Use symmetry for negative arguments
    int sign = 1;
    if (x < 0.0) {
        x = -x;
        sign = -1;
    }
    
    // For large x, use atan(x) = pi/2 - atan(1/x)
    double pi_over_2 = 3.141592653589793 / 2.0;
    if (x > 1.0) {
        return sign * (pi_over_2 - atan_custom(1.0 / x));
    }
    
    // For x > 0.5, use atan(x) = pi/4 + atan((x-1)/(x+1))
    double pi_over_4 = 3.141592653589793 / 4.0;
    if (x > 0.5) {
        return sign * (pi_over_4 + atan_custom((x - 1.0) / (x + 1.0)));
    }
    
    // Taylor series for atan(x)
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    
    for (int i = 1; i <= 50; i = i + 2) {
        result = result + term / i;
        term = term * (-x_squared);
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

The arctangent implementation uses several range reduction techniques. For negative arguments, we use the odd symmetry of arctangent. For arguments greater than one, we use the identity that arctangent of x equals pi over two minus arctangent of one over x. For arguments between 0.5 and one, we use another identity to bring the argument closer to zero.

With the argument sufficiently reduced, we apply the Taylor series, which is x minus x cubed over three plus x to the fifth over five and so on.

IMPLEMENTING HYPERBOLIC FUNCTIONS

Hyperbolic functions are analogs of trigonometric functions based on the hyperbola rather than the circle. They are defined in terms of exponentials and are useful in many scientific applications.

Hyperbolic Sine and Cosine

The hyperbolic sine of x is defined as e to the power x minus e to the power negative x, all divided by two. The hyperbolic cosine is e to the power x plus e to the power negative x, all divided by two.

Here are the implementations:

double sinh_custom(double x) {
    double exp_x = exp_custom(x);
    double exp_neg_x = exp_custom(-x);
    return (exp_x - exp_neg_x) / 2.0;
}

double cosh_custom(double x) {
    double exp_x = exp_custom(x);
    double exp_neg_x = exp_custom(-x);
    return (exp_x + exp_neg_x) / 2.0;
}

double tanh_custom(double x) {
    double exp_2x = exp_custom(2.0 * x);
    return (exp_2x - 1.0) / (exp_2x + 1.0);
}

These implementations are straightforward applications of the definitions. For hyperbolic tangent, we use an algebraically equivalent form that is more numerically stable and efficient.

IMPLEMENTING THE FACTORIAL FUNCTION

The factorial function is defined for non-negative integers. For a positive integer n, n factorial is the product of all positive integers from one to n. By convention, zero factorial is one.

For large values of n, the factorial grows extremely rapidly and will overflow even double precision floating point. We implement factorial for reasonable values and return infinity for values that would overflow.

Here is the implementation:

double factorial_custom(int n) {
    // Handle special cases
    if (n < 0) return 0.0 / 0.0;  // NaN
    if (n == 0 || n == 1) return 1.0;
    if (n > 170) return 1.0 / 0.0;  // Overflow to infinity
    
    // Compute factorial iteratively
    double result = 1.0;
    for (int i = 2; i <= n; i++) {
        result = result * i;
    }
    
    return result;
}

The function returns not a number for negative inputs, as factorial is undefined for negative integers. For n greater than 170, the result would overflow a double, so we return infinity. For valid inputs, we simply multiply all integers from two to n.

IMPLEMENTING COMBINATORIAL FUNCTIONS

Combinatorial functions such as combinations and permutations are useful in probability and statistics. The number of ways to choose k items from n items is n factorial divided by k factorial times n minus k factorial.

Here is the implementation:

double combination_custom(int n, int k) {
    // Handle special cases
    if (k < 0 || k > n || n < 0) return 0.0 / 0.0;  // NaN
    if (k == 0 || k == n) return 1.0;
    
    // Use symmetry: C(n,k) = C(n,n-k)
    if (k > n - k) {
        k = n - k;
    }
    
    // Compute using iterative multiplication and division
    double result = 1.0;
    for (int i = 0; i < k; i++) {
        result = result * (n - i) / (i + 1);
    }
    
    return result;
}

Rather than computing three separate factorials and dividing, which could cause overflow, we compute the combination using a single loop that alternates multiplication and division. This keeps intermediate values smaller and improves numerical stability.

IMPLEMENTING ANGLE CONVERSION

Scientific calculators typically support multiple angle units including degrees, radians, and gradians. We need functions to convert between these units.

A full circle is 360 degrees, two pi radians, or 400 gradians. The conversion functions are straightforward:

double degrees_to_radians(double degrees) {
    return degrees * 3.141592653589793 / 180.0;
}

double radians_to_degrees(double radians) {
    return radians * 180.0 / 3.141592653589793;
}

double gradians_to_radians(double gradians) {
    return gradians * 3.141592653589793 / 200.0;
}

double radians_to_gradians(double radians) {
    return radians * 200.0 / 3.141592653589793;
}

These functions multiply by the appropriate conversion factors. All our trigonometric functions work in radians internally, so these conversions allow users to work in their preferred units.

CALCULATOR ARCHITECTURE

Now that we have implemented all the mathematical functions, we need to design the overall calculator architecture. A scientific calculator needs to parse user input, maintain state, handle operator precedence, and format output.

Expression Parsing

The calculator must parse mathematical expressions entered by the user. This involves tokenizing the input string into numbers, operators, and function names, then evaluating the expression respecting operator precedence and parentheses.

We will use a two-stack algorithm known as the shunting yard algorithm to convert infix notation to postfix notation, which can then be evaluated easily. One stack holds operators and the other holds operands.

State Management

The calculator maintains several pieces of state including the current display value, the angle mode for trigonometric functions, memory storage, and the history of previous calculations. We encapsulate this state in a structure.

Error Handling

Mathematical operations can produce errors such as division by zero, domain errors for functions like logarithm of a negative number, or overflow. The calculator must detect these conditions and report them to the user in a clear manner.

COMPLETE RUNNING EXAMPLE

Below is a complete, production-ready implementation of the scientific calculator. This code includes all the mathematical functions discussed above, a full expression parser, state management, and a command-line interface for user interaction.

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

// Mathematical constants
#define PI 3.141592653589793
#define E 2.718281828459045

// Calculator state structure
typedef struct {
    double memory;
    int angle_mode;  // 0 = radians, 1 = degrees, 2 = gradians
    double last_result;
} CalculatorState;

// Token types for expression parsing
typedef enum {
    TOKEN_NUMBER,
    TOKEN_OPERATOR,
    TOKEN_FUNCTION,
    TOKEN_LPAREN,
    TOKEN_RPAREN,
    TOKEN_END
} TokenType;

typedef struct {
    TokenType type;
    double value;
    char op;
    char func_name[20];
} Token;

// Function prototypes
double exp_custom(double x);
double ln_custom(double x);
double log10_custom(double x);
double power_custom(double x, double y);
double sqrt_custom(double x);
double sin_custom(double x);
double cos_custom(double x);
double tan_custom(double x);
double asin_custom(double x);
double acos_custom(double x);
double atan_custom(double x);
double sinh_custom(double x);
double cosh_custom(double x);
double tanh_custom(double x);
double factorial_custom(int n);
double combination_custom(int n, int k);
double degrees_to_radians(double degrees);
double radians_to_degrees(double radians);
double abs_custom(double x);
double ceil_custom(double x);
double floor_custom(double x);

// Exponential function implementation
double exp_custom(double x) {
    if (x == 0.0) return 1.0;
    if (x < -700.0) return 0.0;
    if (x > 700.0) return 1.0 / 0.0;
    
    int n = (int)x;
    double f = x - n;
    
    if (f > 0.5) {
        n += 1;
        f -= 1.0;
    } else if (f < -0.5) {
        n -= 1;
        f += 1.0;
    }
    
    double result = 1.0;
    double term = 1.0;
    for (int i = 1; i <= 20; i++) {
        term = term * f / i;
        result = result + term;
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    double exp_n = 1.0;
    double base = E;
    int abs_n = n < 0 ? -n : n;
    
    while (abs_n > 0) {
        if (abs_n % 2 == 1) {
            exp_n = exp_n * base;
        }
        base = base * base;
        abs_n = abs_n / 2;
    }
    
    if (n < 0) {
        exp_n = 1.0 / exp_n;
    }
    
    return result * exp_n;
}

// Natural logarithm implementation
double ln_custom(double x) {
    if (x <= 0.0) return -1.0 / 0.0;
    if (x == 1.0) return 0.0;
    
    int exponent = 0;
    while (x >= 2.0) {
        x = x / 2.0;
        exponent = exponent + 1;
    }
    while (x < 1.0) {
        x = x * 2.0;
        exponent = exponent - 1;
    }
    
    double sqrt2 = 1.414213562373095;
    double ln_sqrt2 = 0.346573590279973;
    double adjustment = 0.0;
    
    while (x > 1.2) {
        x = x / sqrt2;
        adjustment = adjustment + ln_sqrt2;
    }
    while (x < 0.9) {
        x = x * sqrt2;
        adjustment = adjustment - ln_sqrt2;
    }
    
    double u = x - 1.0;
    double result = 0.0;
    double term = u;
    
    for (int i = 1; i <= 50; i++) {
        result = result + term / i;
        term = term * (-u);
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    double ln2 = 0.693147180559945;
    return result + adjustment + exponent * ln2;
}

// Base 10 logarithm implementation
double log10_custom(double x) {
    if (x <= 0.0) return -1.0 / 0.0;
    return ln_custom(x) / ln_custom(10.0);
}

// Power function implementation
double power_custom(double x, double y) {
    if (y == 0.0) return 1.0;
    if (x == 0.0) {
        if (y > 0.0) return 0.0;
        return 1.0 / 0.0;
    }
    if (x == 1.0) return 1.0;
    
    int y_int = (int)y;
    if (y == (double)y_int) {
        double result = 1.0;
        int abs_y = y_int < 0 ? -y_int : y_int;
        double base = x;
        
        while (abs_y > 0) {
            if (abs_y % 2 == 1) {
                result = result * base;
            }
            base = base * base;
            abs_y = abs_y / 2;
        }
        
        if (y_int < 0) {
            result = 1.0 / result;
        }
        return result;
    }
    
    if (x < 0.0) {
        return 0.0 / 0.0;
    }
    
    return exp_custom(y * ln_custom(x));
}

// Square root implementation
double sqrt_custom(double x) {
    if (x < 0.0) return 0.0 / 0.0;
    if (x == 0.0) return 0.0;
    if (x == 1.0) return 1.0;
    
    double guess = x / 2.0;
    if (x < 1.0) {
        guess = x;
    }
    
    for (int i = 0; i < 10; i++) {
        double next_guess = 0.5 * (guess + x / guess);
        if (next_guess == guess) break;
        guess = next_guess;
    }
    
    return guess;
}

// Sine function implementation
double sin_custom(double x) {
    while (x >= 2.0 * PI) {
        x = x - 2.0 * PI;
    }
    while (x < 0.0) {
        x = x + 2.0 * PI;
    }
    
    int sign = 1;
    if (x > PI) {
        x = x - PI;
        sign = -sign;
    }
    if (x > PI / 2.0) {
        x = PI - x;
    }
    
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    
    for (int i = 1; i <= 20; i = i + 2) {
        result = result + term;
        term = term * (-x_squared) / ((i + 1) * (i + 2));
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

// Cosine function implementation
double cos_custom(double x) {
    while (x >= 2.0 * PI) {
        x = x - 2.0 * PI;
    }
    while (x < 0.0) {
        x = x + 2.0 * PI;
    }
    
    int sign = 1;
    if (x > PI) {
        x = x - PI;
        sign = -sign;
    }
    if (x > PI / 2.0) {
        x = PI - x;
        sign = -sign;
    }
    
    double result = 0.0;
    double term = 1.0;
    double x_squared = x * x;
    
    for (int i = 0; i <= 20; i = i + 2) {
        result = result + term;
        term = term * (-x_squared) / ((i + 1) * (i + 2));
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

// Tangent function implementation
double tan_custom(double x) {
    double cos_x = cos_custom(x);
    if (cos_x == 0.0) {
        return 1.0 / 0.0;
    }
    return sin_custom(x) / cos_x;
}

// Arcsine function implementation
double asin_custom(double x) {
    if (x < -1.0 || x > 1.0) return 0.0 / 0.0;
    if (x == 0.0) return 0.0;
    if (x == 1.0) return PI / 2.0;
    if (x == -1.0) return -PI / 2.0;
    
    if (x > 0.7) {
        return PI / 2.0 - asin_custom(sqrt_custom(1.0 - x * x));
    }
    if (x < -0.7) {
        return -PI / 2.0 + asin_custom(sqrt_custom(1.0 - x * x));
    }
    
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    double numerator = x;
    double denominator = 1.0;
    
    for (int n = 0; n < 30; n++) {
        result = result + term;
        numerator = numerator * x_squared * (2 * n + 1) * (2 * n + 1);
        denominator = denominator * (2 * n + 2) * (2 * n + 3);
        term = numerator / denominator;
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return result;
}

// Arccosine function implementation
double acos_custom(double x) {
    return PI / 2.0 - asin_custom(x);
}

// Arctangent function implementation
double atan_custom(double x) {
    if (x == 0.0) return 0.0;
    
    int sign = 1;
    if (x < 0.0) {
        x = -x;
        sign = -1;
    }
    
    if (x > 1.0) {
        return sign * (PI / 2.0 - atan_custom(1.0 / x));
    }
    
    if (x > 0.5) {
        return sign * (PI / 4.0 + atan_custom((x - 1.0) / (x + 1.0)));
    }
    
    double result = 0.0;
    double term = x;
    double x_squared = x * x;
    
    for (int i = 1; i <= 50; i = i + 2) {
        result = result + term / i;
        term = term * (-x_squared);
        if (term < 1e-15 && term > -1e-15) break;
    }
    
    return sign * result;
}

// Hyperbolic sine implementation
double sinh_custom(double x) {
    double exp_x = exp_custom(x);
    double exp_neg_x = exp_custom(-x);
    return (exp_x - exp_neg_x) / 2.0;
}

// Hyperbolic cosine implementation
double cosh_custom(double x) {
    double exp_x = exp_custom(x);
    double exp_neg_x = exp_custom(-x);
    return (exp_x + exp_neg_x) / 2.0;
}

// Hyperbolic tangent implementation
double tanh_custom(double x) {
    double exp_2x = exp_custom(2.0 * x);
    return (exp_2x - 1.0) / (exp_2x + 1.0);
}

// Factorial function implementation
double factorial_custom(int n) {
    if (n < 0) return 0.0 / 0.0;
    if (n == 0 || n == 1) return 1.0;
    if (n > 170) return 1.0 / 0.0;
    
    double result = 1.0;
    for (int i = 2; i <= n; i++) {
        result = result * i;
    }
    
    return result;
}

// Combination function implementation
double combination_custom(int n, int k) {
    if (k < 0 || k > n || n < 0) return 0.0 / 0.0;
    if (k == 0 || k == n) return 1.0;
    
    if (k > n - k) {
        k = n - k;
    }
    
    double result = 1.0;
    for (int i = 0; i < k; i++) {
        result = result * (n - i) / (i + 1);
    }
    
    return result;
}

// Angle conversion functions
double degrees_to_radians(double degrees) {
    return degrees * PI / 180.0;
}

double radians_to_degrees(double radians) {
    return radians * 180.0 / PI;
}

// Absolute value implementation
double abs_custom(double x) {
    return x < 0.0 ? -x : x;
}

// Ceiling function implementation
double ceil_custom(double x) {
    int i = (int)x;
    if (x > 0.0 && x > (double)i) {
        return (double)(i + 1);
    }
    return (double)i;
}

// Floor function implementation
double floor_custom(double x) {
    int i = (int)x;
    if (x < 0.0 && x < (double)i) {
        return (double)(i - 1);
    }
    return (double)i;
}

// Tokenizer for expression parsing
Token get_next_token(const char **expr) {
    Token token;
    
    // Skip whitespace
    while (**expr == ' ' || **expr == '\t') {
        (*expr)++;
    }
    
    // Check for end of expression
    if (**expr == '\0') {
        token.type = TOKEN_END;
        return token;
    }
    
    // Check for number
    if (isdigit(**expr) || **expr == '.') {
        char *end;
        token.type = TOKEN_NUMBER;
        token.value = strtod(*expr, &end);
        *expr = end;
        return token;
    }
    
    // Check for parentheses
    if (**expr == '(') {
        token.type = TOKEN_LPAREN;
        (*expr)++;
        return token;
    }
    if (**expr == ')') {
        token.type = TOKEN_RPAREN;
        (*expr)++;
        return token;
    }
    
    // Check for operators
    if (**expr == '+' || **expr == '-' || **expr == '*' || 
        **expr == '/' || **expr == '^' || **expr == '%') {
        token.type = TOKEN_OPERATOR;
        token.op = **expr;
        (*expr)++;
        return token;
    }
    
    // Check for functions
    if (isalpha(**expr)) {
        int i = 0;
        while (isalpha(**expr) && i < 19) {
            token.func_name[i++] = **expr;
            (*expr)++;
        }
        token.func_name[i] = '\0';
        token.type = TOKEN_FUNCTION;
        return token;
    }
    
    // Unknown token
    token.type = TOKEN_END;
    return token;
}

// Operator precedence
int get_precedence(char op) {
    if (op == '+' || op == '-') return 1;
    if (op == '*' || op == '/' || op == '%') return 2;
    if (op == '^') return 3;
    return 0;
}

// Apply binary operator
double apply_operator(double left, double right, char op) {
    switch (op) {
        case '+': return left + right;
        case '-': return left - right;
        case '*': return left * right;
        case '/': 
            if (right == 0.0) return 1.0 / 0.0;
            return left / right;
        case '%': 
            if (right == 0.0) return 0.0 / 0.0;
            return (int)left % (int)right;
        case '^': return power_custom(left, right);
        default: return 0.0;
    }
}

// Apply function
double apply_function(const char *func_name, double arg, CalculatorState *state) {
    // Trigonometric functions
    if (strcmp(func_name, "sin") == 0) {
        if (state->angle_mode == 1) arg = degrees_to_radians(arg);
        return sin_custom(arg);
    }
    if (strcmp(func_name, "cos") == 0) {
        if (state->angle_mode == 1) arg = degrees_to_radians(arg);
        return cos_custom(arg);
    }
    if (strcmp(func_name, "tan") == 0) {
        if (state->angle_mode == 1) arg = degrees_to_radians(arg);
        return tan_custom(arg);
    }
    
    // Inverse trigonometric functions
    if (strcmp(func_name, "asin") == 0) {
        double result = asin_custom(arg);
        if (state->angle_mode == 1) result = radians_to_degrees(result);
        return result;
    }
    if (strcmp(func_name, "acos") == 0) {
        double result = acos_custom(arg);
        if (state->angle_mode == 1) result = radians_to_degrees(result);
        return result;
    }
    if (strcmp(func_name, "atan") == 0) {
        double result = atan_custom(arg);
        if (state->angle_mode == 1) result = radians_to_degrees(result);
        return result;
    }
    
    // Hyperbolic functions
    if (strcmp(func_name, "sinh") == 0) return sinh_custom(arg);
    if (strcmp(func_name, "cosh") == 0) return cosh_custom(arg);
    if (strcmp(func_name, "tanh") == 0) return tanh_custom(arg);
    
    // Exponential and logarithmic functions
    if (strcmp(func_name, "exp") == 0) return exp_custom(arg);
    if (strcmp(func_name, "ln") == 0) return ln_custom(arg);
    if (strcmp(func_name, "log") == 0) return log10_custom(arg);
    
    // Other functions
    if (strcmp(func_name, "sqrt") == 0) return sqrt_custom(arg);
    if (strcmp(func_name, "abs") == 0) return abs_custom(arg);
    if (strcmp(func_name, "ceil") == 0) return ceil_custom(arg);
    if (strcmp(func_name, "floor") == 0) return floor_custom(arg);
    if (strcmp(func_name, "fact") == 0) return factorial_custom((int)arg);
    
    return 0.0 / 0.0;  // Unknown function
}

// Evaluate expression
double evaluate_expression(const char *expr, CalculatorState *state) {
    double operand_stack[100];
    int operand_top = -1;
    
    char operator_stack[100];
    int operator_top = -1;
    
    const char *ptr = expr;
    Token token;
    int expect_operand = 1;
    
    while (1) {
        token = get_next_token(&ptr);
        
        if (token.type == TOKEN_END) {
            break;
        }
        
        if (token.type == TOKEN_NUMBER) {
            operand_stack[++operand_top] = token.value;
            expect_operand = 0;
        }
        else if (token.type == TOKEN_FUNCTION) {
            // Expect opening parenthesis
            token = get_next_token(&ptr);
            if (token.type != TOKEN_LPAREN) {
                return 0.0 / 0.0;  // Error
            }
            
            // Find matching closing parenthesis
            int paren_count = 1;
            const char *start = ptr;
            while (paren_count > 0 && *ptr != '\0') {
                if (*ptr == '(') paren_count++;
                if (*ptr == ')') paren_count--;
                ptr++;
            }
            
            // Extract and evaluate argument
            char arg_expr[200];
            int len = ptr - start - 1;
            strncpy(arg_expr, start, len);
            arg_expr[len] = '\0';
            
            double arg = evaluate_expression(arg_expr, state);
            double result = apply_function(token.func_name, arg, state);
            operand_stack[++operand_top] = result;
            expect_operand = 0;
        }
        else if (token.type == TOKEN_LPAREN) {
            operator_stack[++operator_top] = '(';
        }
        else if (token.type == TOKEN_RPAREN) {
            while (operator_top >= 0 && operator_stack[operator_top] != '(') {
                char op = operator_stack[operator_top--];
                double right = operand_stack[operand_top--];
                double left = operand_stack[operand_top--];
                operand_stack[++operand_top] = apply_operator(left, right, op);
            }
            if (operator_top >= 0) {
                operator_top--;  // Remove '('
            }
        }
        else if (token.type == TOKEN_OPERATOR) {
            // Handle unary minus
            if (token.op == '-' && expect_operand) {
                operand_stack[++operand_top] = 0.0;
                operator_stack[++operator_top] = '-';
                expect_operand = 1;
                continue;
            }
            
            while (operator_top >= 0 && operator_stack[operator_top] != '(' &&
                   get_precedence(operator_stack[operator_top]) >= get_precedence(token.op)) {
                char op = operator_stack[operator_top--];
                double right = operand_stack[operand_top--];
                double left = operand_stack[operand_top--];
                operand_stack[++operand_top] = apply_operator(left, right, op);
            }
            operator_stack[++operator_top] = token.op;
            expect_operand = 1;
        }
    }
    
    // Apply remaining operators
    while (operator_top >= 0) {
        char op = operator_stack[operator_top--];
        if (op == '(') continue;
        double right = operand_stack[operand_top--];
        double left = operand_stack[operand_top--];
        operand_stack[++operand_top] = apply_operator(left, right, op);
    }
    
    return operand_stack[operand_top];
}

// Main calculator interface
int main() {
    CalculatorState state;
    state.memory = 0.0;
    state.angle_mode = 0;  // Radians by default
    state.last_result = 0.0;
    
    char input[500];
    
    printf("Scientific Calculator\n");
    printf("=====================\n");
    printf("Commands:\n");
    printf("  Enter expression to evaluate\n");
    printf("  'deg' - Switch to degree mode\n");
    printf("  'rad' - Switch to radian mode\n");
    printf("  'mem' - Show memory\n");
    printf("  'ms X' - Store X in memory\n");
    printf("  'mr' - Recall memory\n");
    printf("  'mc' - Clear memory\n");
    printf("  'quit' - Exit calculator\n");
    printf("\n");
    printf("Available functions:\n");
    printf("  sin, cos, tan, asin, acos, atan\n");
    printf("  sinh, cosh, tanh\n");
    printf("  exp, ln, log, sqrt\n");
    printf("  abs, ceil, floor, fact\n");
    printf("  Operators: +, -, *, /, ^, %%\n");
    printf("\n");
    
    while (1) {
        printf("> ");
        if (fgets(input, sizeof(input), stdin) == NULL) {
            break;
        }
        
        // Remove newline
        input[strcspn(input, "\n")] = 0;
        
        // Check for commands
        if (strcmp(input, "quit") == 0) {
            break;
        }
        if (strcmp(input, "deg") == 0) {
            state.angle_mode = 1;
            printf("Switched to degree mode\n");
            continue;
        }
        if (strcmp(input, "rad") == 0) {
            state.angle_mode = 0;
            printf("Switched to radian mode\n");
            continue;
        }
        if (strcmp(input, "mem") == 0) {
            printf("Memory: %.10g\n", state.memory);
            continue;
        }
        if (strncmp(input, "ms ", 3) == 0) {
            state.memory = evaluate_expression(input + 3, &state);
            printf("Stored in memory: %.10g\n", state.memory);
            continue;
        }
        if (strcmp(input, "mr") == 0) {
            printf("Memory recall: %.10g\n", state.memory);
            state.last_result = state.memory;
            continue;
        }
        if (strcmp(input, "mc") == 0) {
            state.memory = 0.0;
            printf("Memory cleared\n");
            continue;
        }
        
        // Evaluate expression
        double result = evaluate_expression(input, &state);
        state.last_result = result;
        
        // Check for errors
        if (result != result) {  // NaN
            printf("Error: Invalid operation\n");
        } else if (result == 1.0 / 0.0) {  // Positive infinity
            printf("Error: Result is infinity\n");
        } else if (result == -1.0 / 0.0) {  // Negative infinity
            printf("Error: Result is negative infinity\n");
        } else {
            printf("= %.10g\n", result);
        }
    }
    
    printf("Goodbye!\n");
    return 0;
}

This complete implementation provides a fully functional scientific calculator with all the mathematical functions we discussed. The calculator includes an expression parser that handles operator precedence and parentheses, support for multiple angle modes, memory storage capabilities, and comprehensive error handling.

The code is structured following clean code principles with clear function names, proper separation of concerns, and thorough comments. Each mathematical function is implemented from scratch using only basic arithmetic operations, demonstrating the underlying algorithms and numerical techniques.

Users can enter mathematical expressions using standard infix notation, call functions with parentheses, and use all common mathematical operators. The calculator properly handles edge cases such as division by zero, domain errors, and numerical overflow, providing clear error messages when problems occur.

This implementation serves as both a practical tool and an educational resource, showing how complex mathematical operations can be built up from simple primitives through careful algorithm design and numerical analysis.

No comments: