Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
7f26fac
fix(odr): inline header definitions
NewYaroslav Sep 20, 2025
13bc171
test(odr): rename suite and hook into ci
NewYaroslav Sep 20, 2025
2b8c980
fix(odr): fix ODR violations in headers
NewYaroslav Sep 20, 2025
5b87e37
chore(release): prepare for 1.0.4
NewYaroslav Sep 20, 2025
d719438
chore: update CHANGELOG.md
NewYaroslav Sep 20, 2025
a908da6
fix(time_formatting): correct int64 format specifiers
NewYaroslav Sep 22, 2025
561466e
chore(vcpkg): update port to v1.0.4
NewYaroslav Sep 22, 2025
01004ce
feat(timer): add TimerScheduler for Qt-like timers
NewYaroslav Sep 23, 2025
0293db2
fix(time_utils): refine CPU tick timer semantics
NewYaroslav Sep 23, 2025
32a0ddb
fix(time_utils): refine CPU tick timer semantics
NewYaroslav Sep 23, 2025
3bb2050
refactor(validation): inline workday date calculation
NewYaroslav Sep 23, 2025
864646c
refactor: inline workday date calculation
NewYaroslav Sep 23, 2025
5ee8470
feat(deadline-timer): add ms/sec deadline helpers
NewYaroslav Sep 23, 2025
c693df8
fix(deadline-timer): guard time_point max invocations
NewYaroslav Sep 23, 2025
27b8e36
feat: add ms/sec deadline helpers to DeadlineTimer
NewYaroslav Sep 23, 2025
ba9d170
feat(time_conversions): expose workday helpers
NewYaroslav Sep 30, 2025
7776d30
feat: promote workday helper utilities
NewYaroslav Sep 30, 2025
734ab56
feat(time): add timezone offset helpers and workday month boundaries
NewYaroslav Dec 8, 2025
c41d256
refactor: updated functions and resolved warnings
NewYaroslav Dec 14, 2025
84766cb
chore(changelog): capture new time APIs
NewYaroslav Dec 14, 2025
bf50dec
fix(parser): handle fractional seconds
NewYaroslav Dec 14, 2025
1cc1f3a
fix(time_utils): align realtime clock on unix
NewYaroslav Dec 14, 2025
236244a
chore(changelog): note MQL5 helper additions
NewYaroslav Dec 14, 2025
ba54a41
Update changelog for header split and alias updates
NewYaroslav Dec 15, 2025
75a6a35
feat(conversions): add OA date and astronomy utilities
NewYaroslav Dec 15, 2025
ad55a86
docs: update README for OA and astronomy utilities
NewYaroslav Dec 15, 2025
91a1434
feat(astronomy): add moon phase calculator and helpers
NewYaroslav Dec 15, 2025
f5c1cd9
feat(iso-week): add ISO 8601 week-date support
NewYaroslav Dec 16, 2025
d2a01fb
fix: enforce year bounds in ISO parsing
NewYaroslav Dec 16, 2025
8026097
feat(datetime): add DateTime value type
NewYaroslav Dec 16, 2025
e55fa22
docs: highlight datetime features
NewYaroslav Dec 16, 2025
dca765a
feat(datetime): add validation helpers and mql5 wrapper
NewYaroslav Dec 16, 2025
1dac34f
feat(ntp): add time service and runner offline tests
NewYaroslav Dec 19, 2025
c169ac5
chore: remove unused includes in ntp_client_core.hpp
NewYaroslav Dec 19, 2025
b7a7080
chore: remove include for config.hpp in ntp_packet.hpp
NewYaroslav Dec 19, 2025
e9308e2
chore: remove redundant include of config.hpp
NewYaroslav Dec 19, 2025
04508fa
chore: remove include of config.hpp in udp_transport_posix.hpp
NewYaroslav Dec 19, 2025
6fa2f1b
chore: clean up includes in udp_transport_win.hpp
NewYaroslav Dec 19, 2025
557f05e
chore: remove include of config.hpp in wsa_guard.hpp
NewYaroslav Dec 19, 2025
dcbb4ac
refactor: add includes for WSA and UDP transport in Windows
NewYaroslav Dec 19, 2025
f84b3e8
refactor(ntp): clean up NtpTimeService interface
NewYaroslav Dec 19, 2025
2ed79fa
refactor(ntp): refactor NTP client query and cleanup includes
NewYaroslav Dec 19, 2025
f2d7a99
refactor: add static_assert for NtpPacket size
NewYaroslav Dec 19, 2025
564ac8f
refactor: fix reinterpret_cast usage in setsockopt call
NewYaroslav Dec 19, 2025
a570555
refactor: remove redundant freeaddrinfo calls
NewYaroslav Dec 19, 2025
f58a944
fix(build): silence CI warnings and include time utils for NTP core
NewYaroslav Dec 19, 2025
9adbc43
feat(ntp): harden server response validation and add protocol tests
NewYaroslav Dec 19, 2025
f38806d
docs(ntp): expand Doxygen for NTP client/pool/runner/service and norm…
NewYaroslav Dec 19, 2025
20f51b5
feat(timezone): add US Eastern and Central ↔ GMT conversions with DST…
NewYaroslav Dec 19, 2025
c59d4f1
fix(guards): add leading underscores to ntp headers
NewYaroslav Dec 19, 2025
aff5b74
docs: singleton storage rule; fix NtpTimeService C++17 storage
NewYaroslav Dec 19, 2025
e071a03
perf(time): add fast date conversions (mul-hi), unchecked timestamp A…
NewYaroslav Dec 20, 2025
92043bf
docs(readme): quick start, API invariants, MQL5 coverage and NTP wrap…
NewYaroslav Dec 20, 2025
4f567b6
refactor(types): rename unix day type
NewYaroslav Dec 20, 2025
e68787d
fix(time): make ms conversions overflow-safe (bounds + year-end) and …
NewYaroslav Dec 22, 2025
0fec4e1
fix(tests): add calendar invariants for *_ms boundaries
NewYaroslav Dec 22, 2025
63b1802
fix(tests): use ms_part for negative ms boundary invariants
NewYaroslav Dec 22, 2025
866ed52
fix(tests): replace ms modulo checks with ms_part for pre-epoch year …
NewYaroslav Dec 22, 2025
c966f27
fix(tests): make elapsed timer restart check non-flaky
NewYaroslav Dec 22, 2025
1731380
docs: update CHANGELOG for v1.0.5 release
NewYaroslav Dec 22, 2025
e64eb64
Merge branch 'stable' into main
NewYaroslav Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ jobs:
run: cmake --install build --prefix install
- name: Test
run: ctest --test-dir build
- name: Show CTest last log
if: always()
run: |
echo "=== build/Testing/Temporary/LastTest.log ==="
cat build/Testing/Temporary/LastTest.log || true
- name: Rerun failed tests verbosely
if: always()
run: ctest --test-dir build --rerun-failed --output-on-failure || true
- name: Upload CTest logs
if: always()
uses: actions/upload-artifact@v4
with:
name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }}
path: |
build/Testing/Temporary/LastTest.log
build/Testing/**
- name: Configure consumer project
run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build consumer project
Expand All @@ -41,7 +57,26 @@ jobs:
- name: Install
run: cmake --install build --prefix install --config Release
- name: Test
shell: bash
run: ctest --test-dir build -C Release
- name: Show CTest last log
if: always()
shell: bash
run: |
echo "=== build/Testing/Temporary/LastTest.log ==="
cat build/Testing/Temporary/LastTest.log || true
- name: Rerun failed tests verbosely
if: always()
shell: bash
run: ctest --test-dir build -C Release --rerun-failed --output-on-failure || true
- name: Upload CTest logs
if: always()
uses: actions/upload-artifact@v4
with:
name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }}
path: |
build/Testing/Temporary/LastTest.log
build/Testing/**
- name: Configure consumer project
run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH="${{ github.workspace }}/install" -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build consumer project
Expand All @@ -62,6 +97,22 @@ jobs:
run: cmake --install build --prefix install
- name: Test
run: ctest --test-dir build
- name: Show CTest last log
if: always()
run: |
echo "=== build/Testing/Temporary/LastTest.log ==="
cat build/Testing/Temporary/LastTest.log || true
- name: Rerun failed tests verbosely
if: always()
run: ctest --test-dir build --rerun-failed --output-on-failure || true
- name: Upload CTest logs
if: always()
uses: actions/upload-artifact@v4
with:
name: ctest-logs-${{ runner.os }}-cxx${{ matrix.std }}
path: |
build/Testing/Temporary/LastTest.log
build/Testing/**
- name: Configure consumer project
run: cmake -S tests/install_consumer -B build-consumer -DCMAKE_PREFIX_PATH=${{ github.workspace }}/install -DCMAKE_CXX_STANDARD=${{ matrix.std }}
- name: Build consumer project
Expand Down
86 changes: 86 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,89 @@

- Keep diffs minimal and focused.
- Do not refactor or apply style changes beyond the lines you directly touch.

## Header-only singleton/service storage rule (C++11–C++20)

Use this rule to avoid ODR issues while keeping a single instance per program.

### C++17 and newer
Prefer `static inline` storage inside the class:

```cpp
class Service {
public:
static Service& instance() noexcept {
return s_instance;
}

private:
Service() = default;
static inline Service s_instance{}; // single instance (C++17+)
};
```

### C++11/14 (header-only is not possible without one TU)
You must define storage in exactly one translation unit (TU) using a macro.

Header (`Service.hpp`):

```cpp
#pragma once

class Service {
public:
static Service& instance() noexcept;

private:
Service() = default;
#if __cplusplus >= 201703L
static inline Service s_instance{}; // single instance (C++17+)
#endif
};

#if __cplusplus >= 201703L

inline Service& Service::instance() noexcept {
return s_instance;
}

#else

namespace detail {
# if defined(SERVICE_DEFINE_STORAGE)
Service g_service;
# else
extern Service g_service;
# endif
}

inline Service& Service::instance() noexcept {
return detail::g_service;
}

#endif
```

Usage (C++11/14): define the macro in exactly one `.cpp` file (usually `main.cpp`):

```cpp
#define SERVICE_DEFINE_STORAGE
#include "Service.hpp"
```

All other `.cpp` files should include without the macro:

```cpp
#include "Service.hpp"
```

Notes:
- The macro must be defined in exactly one TU.
- If it is not defined anywhere, you will get an undefined reference error.
- If it is defined in multiple TUs, you will get multiple definition errors.
- In C++17+, the macro is unnecessary (but harmless).
- Use `detail` to keep raw storage out of public API.
- If many services exist in C++11/14, prefer a single TU like `project_singletons.cpp` that defines all `*_DEFINE_STORAGE` macros.

Naming convention for macros:
- `FOO_DEFINE_STORAGE` (or `FOO_IMPLEMENTATION`) — exactly one TU defines it.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

All notable changes to this project will be documented in this file.

## [v1.0.5] - 2025-12-22
- Added fast date conversion paths for timestamp-to-calendar helpers, along with unchecked timestamp math to reduce validation overhead in hot paths (legacy fallbacks remain for comparison).
- Added reference tests and micro-benchmarks for fast vs legacy date conversions with averaged performance measurements across pre/post-epoch ranges.
- Averaged performance gains (legacy/fast, 5 runs): `to_date_time` ~5.68×, `date_to_unix_day` ~1.25×, `to_timestamp` ~1.14×, `to_timestamp_unchecked` ~1.53× (range-dependent).
- Added a `DateTime` value type with fixed UTC offset storage, parsing, formatting, arithmetic helpers, and examples/tests.
- Added OA date conversions and astronomy helpers (JD/MJD/JDN, lunar phase/age) with docs and examples.
- Added ISO week-date conversions, formatting, and parsing utilities.
- Added geocentric MoonPhase calculator with quarter timings, documentation, and tests.
- Added continuous lunar phase sin/cos helpers, structured quarter instants with event windows, documentation, and tests.
- Split `time_conversions.hpp` into modular headers while keeping the umbrella include, preserving APIs with compatibility aliases and refreshed docs.
- Added short-form weekday and timestamp conversion aliases alongside new constexpr timezone offset helpers.
- Expanded conversion coverage tests to exercise the renamed helpers and new wrappers across C++11/14/17 builds.
- Documented first/last workday boundary helpers and their UTC semantics in README and Doxygen mainpage.
- Renamed `uday_t` to `dse_t` and added `unix_day_t`/`unixday_t` aliases for backward compatibility.
- Corrected unix-day millisecond alias helpers to reuse `days_since_epoch_ms` logic.
- Added workday boundary coverage and alias verification to `time_conversions_test`.
- Introduced UTC offset helpers (`to_utc`/`to_local`, seconds and milliseconds) and TimeZoneStruct offset extraction.
- Expanded time parsing and workday/date conversion APIs: new month/workday inspectors, `unix_day_to_ts*` aliases, `days_since_epoch*`/`min_since_epoch`, and renamed year accessors to `years_since_epoch`/`year_of*`.
- Added POSIX `NtpClient` implementation with Unix test coverage and refreshed NTP documentation.
- Unified `now_realtime_us()` precision across Windows and Unix by combining realtime anchors with monotonic clocks.
- Added MQL5 counterparts for recent workday boundary and ISO parsing helpers.
- Introduced templated NTP client pools, offline/fake testing paths, background runner helpers, and a singleton NTP time service with convenience wrappers.
- Added US Eastern Time (ET/NY) to GMT (UTC) conversion helpers with DST rules.
- Added US Central Time (CT) to GMT (UTC) conversion helpers for America/Chicago.

## [v1.0.4] - 2025-09-20
- fix ODR violations in headers

Expand Down
11 changes: 5 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.15)
project(TimeShield VERSION 1.0.4 LANGUAGES CXX)
project(TimeShield VERSION 1.0.5 LANGUAGES CXX)

if(NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 11)
Expand All @@ -21,11 +21,7 @@ target_include_directories(
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

if(WIN32)
set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT ON)
else()
set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT OFF)
endif()
set(TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT ON)
option(TIME_SHIELD_ENABLE_NTP_CLIENT "Enable NTP client" ${TIME_SHIELD_ENABLE_NTP_CLIENT_DEFAULT})

target_compile_definitions(
Expand Down Expand Up @@ -110,6 +106,9 @@ if(TIME_SHIELD_CPP_BUILD_TESTS)
get_filename_component(test_name ${test_src} NAME_WE)
add_executable(${test_name} ${test_src})
target_link_libraries(${test_name} PRIVATE time_shield::time_shield)
if(WIN32)
target_link_libraries(${test_name} PRIVATE ws2_32)
endif()
if(COMMON_WARN_FLAGS)
target_compile_options(${test_name} PRIVATE ${COMMON_WARN_FLAGS})
endif()
Expand Down
3 changes: 3 additions & 0 deletions MQL5/Include/time_shield.mqh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
// Structure representing date and time combinations
#include <time_shield/date_time_struct.mqh>

// Value-type wrapper for date-time with offset
#include <time_shield/DateTime.mqh>

// Functions for validation of time-related values
#include <time_shield/validation.mqh>

Expand Down
144 changes: 144 additions & 0 deletions MQL5/Include/time_shield/DateTime.mqh
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//+------------------------------------------------------------------+
//| DateTime.mqh |
//| Time Shield - MQL5 DateTime Type |
//| Copyright 2025, NewYaroslav |
//| https://github.com/NewYaroslav/time-shield-cpp |
//+------------------------------------------------------------------+
#ifndef __TIME_SHIELD_DATE_TIME_MQH__
#define __TIME_SHIELD_DATE_TIME_MQH__

/// \file DateTime.mqh
/// \ingroup mql5
/// \brief Lightweight date-time wrapper for MQL5 with fixed UTC offset.

#property copyright "Copyright 2025, NewYaroslav"
#property link "https://github.com/NewYaroslav/time-shield-cpp"
#property strict

#include "time_conversions.mqh"
#include "time_formatting.mqh"
#include "time_parser.mqh"
#include "time_zone_struct.mqh"
#include "validation.mqh"

namespace time_shield {

/// \brief Represents a UTC timestamp with an optional fixed offset.
class DateTime {
public:
/// \brief Default constructor initializes epoch with zero offset.
DateTime(): m_utc_ms(0), m_offset(0) {}

/// \brief Create instance from UTC milliseconds.
/// \param utc_ms Timestamp in milliseconds since the Unix epoch (UTC).
/// \param offset Fixed UTC offset in seconds.
/// \return Constructed DateTime.
static DateTime from_unix_ms(const long utc_ms, const int offset = 0) {
return DateTime(utc_ms, offset);
}

/// \brief Construct instance for current UTC time.
/// \param offset Fixed UTC offset in seconds.
/// \return DateTime set to now.
static DateTime now_utc(const int offset = 0) {
return DateTime(ts_ms(), offset);
}

/// \brief Try to build from calendar components interpreted in provided offset.
/// \param year Year component.
/// \param month Month component.
/// \param day Day component.
/// \param hour Hour component.
/// \param min Minute component.
/// \param sec Second component.
/// \param ms Millisecond component.
/// \param offset Fixed UTC offset in seconds.
/// \param out Output DateTime on success.
/// \return True when components form a valid date-time and offset.
static bool try_from_components(
const long year,
const int month,
const int day,
const int hour,
const int min,
const int sec,
const int ms,
const int offset,
DateTime &out) {
if (!is_valid_date_time(year, month, day, hour, min, sec, ms))
return false;

TimeZoneStruct tz = to_time_zone_struct(offset);
if (!is_valid_time_zone_offset(tz))
return false;

const long local_ms = to_timestamp_ms(year, month, day, hour, min, sec, ms);
out = DateTime(local_ms - offset_to_ms(offset), offset);
return true;
}

/// \brief Try to build from DateTimeStruct interpreted in provided offset.
/// \param local_dt Local date-time structure.
/// \param offset Fixed UTC offset in seconds.
/// \param out Output DateTime on success.
/// \return True when structure and offset are valid.
static bool try_from_date_time_struct(
const DateTimeStruct &local_dt,
const int offset,
DateTime &out) {
if (!is_valid_date_time(local_dt))
return false;

TimeZoneStruct tz = to_time_zone_struct(offset);
if (!is_valid_time_zone_offset(tz))
return false;

const long local_ms = dt_to_timestamp_ms(local_dt);
out = DateTime(local_ms - offset_to_ms(offset), offset);
return true;
}

/// \brief Try to parse ISO8601 string to DateTime.
/// \param str Input ISO8601 string.
/// \param out Output DateTime when parsing succeeds.
/// \return True on success.
static bool try_parse_iso8601(const string str, DateTime &out) {
DateTimeStruct dt = create_date_time_struct(0);
TimeZoneStruct tz = create_time_zone_struct(0, 0, true);
if (!parse_iso8601(str, dt, tz))
return false;

out = DateTime(dt_to_timestamp_ms(dt) - offset_to_ms(time_zone_struct_to_offset(tz)),
time_zone_struct_to_offset(tz));
return true;
}

/// \brief Format to ISO8601 string with stored offset.
string to_iso8601() const { return to_iso8601_ms(local_ms(), m_offset); }

/// \brief Access UTC milliseconds.
long unix_ms() const { return m_utc_ms; }

/// \brief Access stored UTC offset.
int utc_offset() const { return m_offset; }

/// \brief Return copy with new offset preserving instant.
DateTime with_offset(const int new_offset) const { return DateTime(m_utc_ms, new_offset); }

/// \brief Return copy with zero offset.
DateTime to_utc() const { return with_offset(0); }

private:
DateTime(const long utc_ms, const int offset): m_utc_ms(utc_ms), m_offset(offset) {}

static long offset_to_ms(const int offset) { return (long)offset * MS_PER_SEC; }

long local_ms() const { return m_utc_ms + offset_to_ms(m_offset); }

long m_utc_ms;
int m_offset;
};

} // namespace time_shield

#endif // __TIME_SHIELD_DATE_TIME_MQH__
Loading
Loading