David Shaheen
David Shaheen

Reputation: 1

Time zone problem with OpenWeatherMap App

I'm working on a basic weather app that takes a US zip code and gives the 5 day forecast including "today" based on the particular zip code's "today" regardless of where the app user is searching from. I'm using the OpenWeather api. Here is the app hosted on Render (forgive the 30-50 second initial load time).

Problem:

I think the way it's handling the time zone is messed up because sometimes it thinks that "Today" is actually tomorrow. For instance, I'm in MST and it's Monday. It's 4:03pm MST right now. I just searched the weather for 85338 (also in MST) and it has "Today" and then the next day is Wednesday (which means the app is using Tuesday as "Today." Note the app is hosted on Render using their Oregon location. But, I still had this problem when I was testing locally. At first I thought it was only an issue when I search for an Eastern Time location that was in a different day than me (like when I tested the app at 11pm MST and it was the next day in Eastern time), but it also has issues in the middle of the day too (as noted above). So I'm not sure what to do. Below is my code for my main files and a screen shot of one example of the issue.

Here's the full project on GitHub.

index.js

import express from "express";
import axios from "axios";
import bodyParser from "body-parser";
import dotenv from "dotenv";
import moment from "moment-timezone";

// Load environment variables from .env file
dotenv.config();

const app = express();
const port = 3000;
const baseApiUrl = "http://api.openweathermap.org";

const apiKey = process.env.OPENWEATHER_API_KEY;

app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static('public'));

app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  res.render('index', { activePage: 'home', location: null, forecast: null, error: null, content: 'Enter a postal code to get the current weather.' });
});

app.get('/about', (req, res) => {
  res.render('about', {
    activePage: 'about'
  });
});

app.get('/getWeather', async (req, res) => {
  const zip = req.query.zip;
  try {
    // Use postal code without specifying the country, so OpenWeatherMap can determine the location
    const weatherResponse = await axios.get(`${baseApiUrl}/data/2.5/forecast?zip=${zip}&appid=${apiKey}&units=imperial`);
    const forecast = weatherResponse.data;

    // Extract city and country
    const city = forecast.city.name;
    const country = forecast.city.country;
    const location = `${city}, ${country}`;

    // Get the timezone offset from the API response (in seconds)
    const timezoneOffset = forecast.city.timezone;

    // Get the current local date in the city's time zone
    const today = moment.utc().add(timezoneOffset, 'seconds').format('YYYY-MM-DD');

    // Group forecasts by day and calculate high and low temperatures
    const forecastByDay = {};

    forecast.list.forEach(item => {
      // Adjust each forecast item's timestamp to the local date using the timezone offset
      const localDate = moment.utc(item.dt * 1000).add(timezoneOffset, 'seconds').format('YYYY-MM-DD');
      if (!forecastByDay[localDate]) {
        forecastByDay[localDate] = { tempMin: item.main.temp, tempMax: item.main.temp, weather: [] };
      } else {
        if (item.main.temp < forecastByDay[localDate].tempMin) {
          forecastByDay[localDate].tempMin = item.main.temp;
        }
        if (item.main.temp > forecastByDay[localDate].tempMax) {
          forecastByDay[localDate].tempMax = item.main.temp;
        }
      }
      forecastByDay[localDate].weather.push(item.weather[0]);
    });

    const dailyForecasts = Object.keys(forecastByDay).sort().map(date => {
      const dayData = forecastByDay[date];
      const weatherCounts = dayData.weather.reduce((acc, weather) => {
        const key = weather.icon.replace('n', 'd'); // Force day icon
        if (!acc[key]) {
          acc[key] = { count: 0, weather };
        }
        acc[key].count += 1;
        return acc;
      }, {});
      const mostFrequentWeather = Object.values(weatherCounts).sort((a, b) => b.count - a.count)[0].weather;

      // Capitalize the first letter of the weather description
      const description = mostFrequentWeather.description.charAt(0).toUpperCase() + mostFrequentWeather.description.slice(1);

      return {
        date,
        tempMin: Math.round(dayData.tempMin),
        tempMax: Math.round(dayData.tempMax),
        weather: {
          ...mostFrequentWeather,
          description: description,
          icon: mostFrequentWeather.icon.replace('n', 'd') // Ensure day icon
        }
      };
    });

    // Filter forecasts to show only today and the next four days
    const filteredForecasts = dailyForecasts.filter(forecast => moment(forecast.date).isAfter(today)).slice(0, 5);

    res.render('index', { activePage: 'home', location: location, forecast: filteredForecasts, error: null });
  } catch (error) {
    console.error(error);
    res.render('index', { activePage: 'home', location: null, forecast: null, error: 'Error fetching data. Please try again.', content: null });
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <%- include("partials/head.ejs") %>
</head>
<body>
  <%- include("partials/header.ejs") %>

  <div class="container mt-5">
    <div class="d-flex justify-content-end mt-4">
      <div class="text-right">
        <label class="switch">
          <input type="checkbox" id="unitToggle" checked>
          <span class="slider round"></span>
        </label>
        <span id="unitLabel">°F</span>
      </div>
    </div>
    <div class="text-center mt-4">
      <h1 class="text-center">Weather App</h1>
      <form action="/getWeather" method="GET" class="d-inline-block w-100">
        <div class="form-group d-flex justify-content-center">
          <input type="text" class="form-control" id="zip" name="zip" placeholder="Enter zip code" required>
        </div>
        <div class="form-group d-flex justify-content-center">
          <button type="submit" class="btn btn-primary">Get Weather</button>
        </div>
      </form>
    </div>

    <% if (location) { %>
      <h2 class="text-center mt-4 location"><%= location %></h2>
    <% } %>

    <% if (forecast) { %>
      <div class="response-area mt-4">
        <h2 class="text-center">5-Day Forecast</h2>
        <div class="row text-center justify-content-center">
          <% forecast.forEach(function(day, index) { %>
            <div class="col-12 col-md-2 forecast-item">
              <div class="forecast-content">
                <h5>
                  <% if (index === 0) { %>
                    Today
                  <% } else { %>
                    <%= new Date(day.date).toLocaleDateString('en-US', { weekday: 'long' }) %>
                  <% } %>
                </h5>
                <p>
                  <span class="temp-max"><%= day.tempMax %></span>°<span class="unit">F</span> / 
                  <span class="temp-min"><%= day.tempMin %></span>°<span class="unit">F</span>
                </p>
                <p><%= day.weather.description %></p>
                <img src="http://openweathermap.org/img/wn/<%= day.weather.icon %>@2x.png" alt="<%= day.weather.description %>">
              </div>
            </div>
          <% }); %>
        </div>
      </div>
    <% } %>

    <% if (error) { %>
      <div class="alert alert-danger mt-4"><%= error %></div>
    <% } %>
  </div>

  <%- include("partials/footer.ejs") %>

  <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const unitToggle = document.getElementById('unitToggle');
      const unitLabel = document.getElementById('unitLabel');
      const tempElements = document.querySelectorAll('.temp-max, .temp-min');
      const unitElements = document.querySelectorAll('.unit');

      unitToggle.addEventListener('change', function() {
        const isImperial = unitToggle.checked;
        unitLabel.textContent = isImperial ? '°F' : '°C';

        tempElements.forEach((tempElement, index) => {
          const currentTemp = parseFloat(tempElement.textContent);
          if (isImperial) {
            // Convert from Celsius to Fahrenheit
            tempElement.textContent = Math.round((currentTemp * 9/5) + 32);
            unitElements[index].textContent = 'F';
          } else {
            // Convert from Fahrenheit to Celsius
            tempElement.textContent = Math.round((currentTemp - 32) * 5/9);
            unitElements[index].textContent = 'C';
          }
        });
      });
    });
  </script>
</body>
</html>

I have tried using the moment-timezone library to adjust the forecast timestamps to the local time of the city for which the weather is being retrieved. My expectation is to display the forecast dates accurately based on the local time of the city.

Here's a snippet of my code:

const forecast = weatherResponse.data;
const timezoneOffset = forecast.city.timezone;
const today = moment.utc().add(timezoneOffset, 'seconds').format('YYYY-MM-DD');

const forecastByDay = {};
forecast.list.forEach(item => {
  const localDate = moment.utc(item.dt * 1000).add(timezoneOffset, 'seconds').format('YYYY-MM-DD');
  if (!forecastByDay[localDate]) {
    forecastByDay[localDate] = { tempMin: item.main.temp, tempMax: item.main.temp, weather: [] };
  } else {
    if (item.main.temp < forecastByDay[localDate].tempMin) {
      forecastByDay[localDate].tempMin = item.main.temp;
    }
    if (item.main.temp > forecastByDay[localDate].tempMax) {
      forecastByDay[localDate].tempMax = item.main.temp;
    }
  }
  forecastByDay[localDate].weather.push(item.weather[0]);
});

Despite this, the dates are not aligning with the expected local dates. Could someone help me understand what might be going wrong or suggest a better approach to handle the time zone conversion accurately? Any help is appreciated!

Upvotes: 0

Views: 61

Answers (0)

Related Questions