// SPDX-License-Identifier: GPL-3.0-or-later WITH STruCpp-runtime-exception // Copyright (C) 2025 Autonomy / OpenPLC Project // This file is part of the STruC++ Runtime Library and is covered by the // STruC++ Runtime Library Exception. See COPYING.RUNTIME for details. // // ============================================================================ // WARNING: KEEP THIS HEADER C++14-CLEAN. // ---------------------------------------------------------------------------- // strucpp targets C++17 and its EMITTED code is compiled as C++17 — but this // header is part of the C/C++ Function Block include chain, which is NOT. // OpenPLC Editor's Arduino flow emits `c_blocks_code.cpp`, including // `iec_var.hpp` + `iec_string.hpp` (which transitively pull in `iec_traits.hpp` // and `iec_types.hpp`). That translation unit is compiled under whatever // `-std=` the Arduino core picks, and every mbed-based core — Nano RP2040 // Connect, Nano 33 BLE, Opta, GIGA, Portenta, Edge — hard-codes `-std=gnu++14`. // So any C++17/20 construct reachable from here breaks the user's C/C++ POU // build, even though the rest of strucpp is happily on C++17. // // In this header (and anything it includes) do NOT use C++17/20 features // unguarded. In particular: // * `std::trait_v` -> `std::trait::value` // * `if constexpr` -> SFINAE / tag dispatch // * inline variables / `inline constexpr` // * `auto` non-type template params -> typed NTTPs // * C++17/20 library headers (, , , // , ...) -> include ONLY behind `#if __cplusplus >= ...` // (see the guarded block in iec_types.hpp for the pattern). // // Boundary introduced in commit be85d8a. If you change which headers // `c_blocks_code.cpp` pulls in, update this set of warnings accordingly. // ============================================================================ /** * STruC++ Runtime - IEC String Types * * This header provides the IEC 61131-3 STRING type as a fixed-length string template. * STRING[n] represents a string with maximum length n (default 254 per IEC standard). * The implementation avoids dynamic memory allocation for real-time safety. */ #pragma once #include #include #include #include #include #include #include "iec_types.hpp" #include "iec_var.hpp" #include "iec_traits.hpp" namespace strucpp { // Forward declaration for cross-type assignment template class IECStringVar; template class IECString { public: static constexpr size_t max_length = MaxLen; using value_type = CHAR_t; using size_type = size_t; constexpr IECString() noexcept : length_(0) { data_[0] = '\0'; } IECString(const char* str) noexcept : length_(0) { if (str) { size_t len = std::strlen(str); length_ = static_cast(len < MaxLen ? len : MaxLen); std::memcpy(data_, str, length_); } data_[length_] = '\0'; } IECString(const char* str, size_t len) noexcept { length_ = static_cast(len < MaxLen ? len : MaxLen); std::memcpy(data_, str, length_); data_[length_] = '\0'; } template IECString(const IECString& other) noexcept { length_ = static_cast(other.length() < MaxLen ? other.length() : MaxLen); std::memcpy(data_, other.c_str(), length_); data_[length_] = '\0'; } IECString(const IECString&) = default; IECString(IECString&&) = default; IECString& operator=(const IECString&) = default; IECString& operator=(IECString&&) = default; IECString& operator=(const char* str) noexcept { if (str) { size_t len = std::strlen(str); length_ = static_cast(len < MaxLen ? len : MaxLen); std::memcpy(data_, str, length_); } else { length_ = 0; } data_[length_] = '\0'; return *this; } template IECString& operator=(const IECString& other) noexcept { length_ = static_cast(other.length() < MaxLen ? other.length() : MaxLen); std::memcpy(data_, other.c_str(), length_); data_[length_] = '\0'; return *this; } // Cross-type assignment from IECStringVar (defined after IECStringVar class) template inline IECString& operator=(const IECStringVar& other) noexcept; constexpr size_t length() const noexcept { return length_; } constexpr size_t size() const noexcept { return length_; } constexpr size_t capacity() const noexcept { return MaxLen; } constexpr bool empty() const noexcept { return length_ == 0; } const char* c_str() const noexcept { return data_; } const char* data() const noexcept { return data_; } char* data() noexcept { return data_; } char operator[](size_t index) const noexcept { return index < length_ ? data_[index] : '\0'; } char& operator[](size_t index) noexcept { return data_[index < length_ ? index : length_]; } char at(size_t index) const noexcept { return index < length_ ? data_[index] : '\0'; } void clear() noexcept { length_ = 0; data_[0] = '\0'; } void resize(size_t new_len) noexcept { if (new_len > MaxLen) new_len = MaxLen; if (new_len > length_) { std::memset(data_ + length_, ' ', new_len - length_); } length_ = static_cast(new_len); data_[length_] = '\0'; } template IECString& append(const IECString& other) noexcept { size_t copy_len = other.length(); if (length_ + copy_len > MaxLen) { copy_len = MaxLen - length_; } std::memcpy(data_ + length_, other.c_str(), copy_len); length_ += static_cast(copy_len); data_[length_] = '\0'; return *this; } IECString& append(const char* str) noexcept { if (str) { size_t str_len = std::strlen(str); size_t copy_len = str_len; if (length_ + copy_len > MaxLen) { copy_len = MaxLen - length_; } std::memcpy(data_ + length_, str, copy_len); length_ += static_cast(copy_len); data_[length_] = '\0'; } return *this; } IECString& append(char c) noexcept { if (length_ < MaxLen) { data_[length_++] = c; data_[length_] = '\0'; } return *this; } template IECString operator+(const IECString& other) const noexcept { IECString result(*this); result.append(other); return result; } IECString operator+(const char* str) const noexcept { IECString result(*this); result.append(str); return result; } template IECString& operator+=(const IECString& other) noexcept { return append(other); } IECString& operator+=(const char* str) noexcept { return append(str); } IECString& operator+=(char c) noexcept { return append(c); } template bool operator==(const IECString& other) const noexcept { if (length_ != other.length()) return false; return std::memcmp(data_, other.c_str(), length_) == 0; } bool operator==(const char* str) const noexcept { if (!str) return length_ == 0; return std::strcmp(data_, str) == 0; } template bool operator!=(const IECString& other) const noexcept { return !(*this == other); } bool operator!=(const char* str) const noexcept { return !(*this == str); } template bool operator<(const IECString& other) const noexcept { return std::strcmp(data_, other.c_str()) < 0; } template bool operator<=(const IECString& other) const noexcept { return std::strcmp(data_, other.c_str()) <= 0; } template bool operator>(const IECString& other) const noexcept { return std::strcmp(data_, other.c_str()) > 0; } template bool operator>=(const IECString& other) const noexcept { return std::strcmp(data_, other.c_str()) >= 0; } template int compare(const IECString& other) const noexcept { return std::strcmp(data_, other.c_str()); } int compare(const char* str) const noexcept { return std::strcmp(data_, str ? str : ""); } template size_t find(const IECString& substr, size_t pos = 0) const noexcept { if (pos >= length_ || substr.length() == 0) return npos; const char* found = std::strstr(data_ + pos, substr.c_str()); return found ? static_cast(found - data_) : npos; } size_t find(const char* substr, size_t pos = 0) const noexcept { if (pos >= length_ || !substr || !*substr) return npos; const char* found = std::strstr(data_ + pos, substr); return found ? static_cast(found - data_) : npos; } size_t find(char c, size_t pos = 0) const noexcept { for (size_t i = pos; i < length_; ++i) { if (data_[i] == c) return i; } return npos; } IECString substr(size_t pos, size_t len = npos) const noexcept { if (pos >= length_) return IECString(); if (len == npos || pos + len > length_) { len = length_ - pos; } return IECString(data_ + pos, len); } void replace(size_t pos, size_t len, const char* str) noexcept { if (pos >= length_) return; if (pos + len > length_) len = length_ - pos; size_t str_len = str ? std::strlen(str) : 0; size_t new_len = length_ - len + str_len; if (new_len > MaxLen) { str_len = MaxLen - (length_ - len); new_len = MaxLen; } if (str_len != len) { std::memmove(data_ + pos + str_len, data_ + pos + len, length_ - pos - len); } if (str_len > 0 && str) { std::memcpy(data_ + pos, str, str_len); } length_ = static_cast(new_len); data_[length_] = '\0'; } void insert(size_t pos, const char* str) noexcept { if (pos > length_) pos = length_; if (!str) return; size_t str_len = std::strlen(str); if (length_ + str_len > MaxLen) { str_len = MaxLen - length_; } std::memmove(data_ + pos + str_len, data_ + pos, length_ - pos); std::memcpy(data_ + pos, str, str_len); length_ += static_cast(str_len); data_[length_] = '\0'; } void erase(size_t pos, size_t len = npos) noexcept { if (pos >= length_) return; if (len == npos || pos + len > length_) { len = length_ - pos; } std::memmove(data_ + pos, data_ + pos + len, length_ - pos - len); length_ -= static_cast(len); data_[length_] = '\0'; } static constexpr size_t npos = static_cast(-1); private: char data_[MaxLen + 1]; uint16_t length_; }; using STRING = IECString<254>; template class IECStringVar { public: using value_type = IECString; IECStringVar() noexcept : value_{}, forced_{false}, forced_value_{} {} IECStringVar(const value_type& v) noexcept : value_{v}, forced_{false}, forced_value_{} {} IECStringVar(const char* str) noexcept : value_{str}, forced_{false}, forced_value_{} {} IECStringVar(const IECStringVar&) = default; IECStringVar(IECStringVar&&) = default; IECStringVar& operator=(const IECStringVar&) = default; IECStringVar& operator=(IECStringVar&&) = default; // Cross-size converting constructor (IEC 61131-3: STRING types are interoperable) template = 0> IECStringVar(const IECStringVar& other) noexcept : value_(other.get().c_str()), forced_{false}, forced_value_{} {} // Converting constructor from IECString of any size template = 0> IECStringVar(const IECString& other) noexcept : value_(other.c_str()), forced_{false}, forced_value_{} {} // Converting constructor from IECVar> (struct field access returns this type) template IECStringVar(const IECVar>& other) noexcept : value_(static_cast>(other).c_str()), forced_{false}, forced_value_{} {} // Cross-size assignment (IEC 61131-3: STRING types are interoperable, truncation on overflow) template IECStringVar& operator=(const IECStringVar& other) noexcept { value_ = IECString(other.get().c_str()); return *this; } // Assignment from IECVar> (struct field access) template IECStringVar& operator=(const IECVar>& other) noexcept { value_ = IECString(static_cast>(other).c_str()); return *this; } // Assignment from IECString of different size template = 0> IECStringVar& operator=(const IECString& other) noexcept { value_ = IECString(other.c_str()); return *this; } value_type get() const noexcept { return forced_ ? forced_value_ : value_; } void set(const value_type& v) noexcept { value_ = v; } void set(const char* str) noexcept { value_ = str; } value_type get_underlying() const noexcept { return value_; } void force(const value_type& v) noexcept { forced_ = true; forced_value_ = v; } void force(const char* str) noexcept { forced_ = true; forced_value_ = str; } void unforce() noexcept { forced_ = false; } bool is_forced() const noexcept { return forced_; } value_type get_forced_value() const noexcept { return forced_value_; } operator value_type() const noexcept { return get(); } IECStringVar& operator=(const value_type& v) noexcept { set(v); return *this; } IECStringVar& operator=(const char* str) noexcept { set(str); return *this; } // Read-through proxies into the inner IECString. Without these, // user code in C/C++ POU bodies has to write `name.get().c_str()` // / `name.get().length()` for every read — `IECStringVar` would // otherwise expose nothing but `get()` / `set()` / `operator=` to // its consumers. Routed through the force-aware value (forced // value when forcing is active, underlying value otherwise) so // every read respects force semantics the same way `IECVar`'s // `operator T()` already does for numeric pins. Returning the // pointer to the persistent member (not to `get()`'s temporary) // keeps `c_str()`'s buffer stable for the lifetime of `*this`. // // NOTE: only methods that didn't previously exist on // `IECStringVar` are added here. Comparison (`operator==` etc.) // is intentionally NOT proxied — the free-function overloads at // the bottom of this file (`operator==(IECStringVar, IECString)` // and converse) already handle every cross-class compare path, // and adding member operators would risk overload ambiguity at // ST call sites that previously bound to the free functions. constexpr size_t length() const noexcept { return (forced_ ? forced_value_ : value_).length(); } const char* c_str() const noexcept { return (forced_ ? forced_value_ : value_).c_str(); } char operator[](size_t index) const noexcept { return (forced_ ? forced_value_ : value_)[index]; } private: value_type value_; bool forced_; value_type forced_value_; }; using STRING_VAR = IECStringVar<254>; // Deferred definition: IECString::operator=(const IECStringVar&) // Template deduction doesn't consider user-defined conversions, so we need // this explicit assignment to handle: rawStringField = stringVar template template inline IECString& IECString::operator=(const IECStringVar& other) noexcept { auto val = other.get(); length_ = static_cast(val.length() < MaxLen ? val.length() : MaxLen); std::memcpy(data_, val.c_str(), length_); data_[length_] = '\0'; return *this; } // Comparison operators between IECString and IECStringVar // (template deduction doesn't use implicit conversions) template inline bool operator==(const IECString& a, const IECStringVar& b) noexcept { return a == b.get(); } template inline bool operator==(const IECStringVar& a, const IECString& b) noexcept { return a.get() == b; } template inline bool operator==(const IECStringVar& a, const IECStringVar& b) noexcept { return a.get() == b.get(); } template inline bool operator!=(const IECString& a, const IECStringVar& b) noexcept { return !(a == b); } template inline bool operator!=(const IECStringVar& a, const IECString& b) noexcept { return !(a == b); } template inline bool operator!=(const IECStringVar& a, const IECStringVar& b) noexcept { return !(a == b); } // Comparison operators for IECVar> (struct field access returns this type) // Template deduction won't chain through IECVar::operator T() + IECString comparison template inline bool operator==(const IECVar>& a, const IECStringVar& b) noexcept { return static_cast>(a) == b.get(); } template inline bool operator==(const IECStringVar& a, const IECVar>& b) noexcept { return a.get() == static_cast>(b); } template inline bool operator!=(const IECVar>& a, const IECStringVar& b) noexcept { return !(a == b); } template inline bool operator!=(const IECStringVar& a, const IECVar>& b) noexcept { return !(a == b); } // Comparison with const char* template inline bool operator==(const IECStringVar& a, const char* b) noexcept { return a.get() == b; } template inline bool operator==(const char* a, const IECStringVar& b) noexcept { return b.get() == a; } template inline bool operator!=(const IECStringVar& a, const char* b) noexcept { return !(a == b); } template inline bool operator!=(const char* a, const IECStringVar& b) noexcept { return !(a == b); } // Ordering operators for IECStringVar template inline bool operator<(const IECStringVar& a, const IECStringVar& b) noexcept { return a.get() < b.get(); } template inline bool operator<(const IECString& a, const IECStringVar& b) noexcept { return a < b.get(); } template inline bool operator<(const IECStringVar& a, const IECString& b) noexcept { return a.get() < b; } template inline bool operator>(const IECStringVar& a, const IECStringVar& b) noexcept { return b < a; } template inline bool operator>(const IECString& a, const IECStringVar& b) noexcept { return b < a; } template inline bool operator>(const IECStringVar& a, const IECString& b) noexcept { return b < a; } // Non-template alias for codegen: IEC_STRING = IECStringVar<254> // For parameterized STRING(N), codegen emits IECStringVar directly using IEC_STRING = IECStringVar<254>; template inline size_t LEN(const IECString& s) noexcept { return s.length(); } // IECStringVar overload: template deduction doesn't go through implicit conversions template inline size_t LEN(const IECStringVar& s) noexcept { return s.get().length(); } template inline IECString LEFT(const IECString& s, size_t len) noexcept { return s.substr(0, len); } template inline IECString RIGHT(const IECString& s, size_t len) noexcept { if (len >= s.length()) return s; return s.substr(s.length() - len, len); } template inline IECString MID(const IECString& s, size_t len, size_t pos) noexcept { if (pos == 0) return IECString(); return s.substr(pos - 1, len); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECString& s1, const IECString& s2) noexcept { constexpr size_t ResultLen = MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2; IECString result(s1); result.append(s2); return result; } // Variadic CONCAT for 3+ arguments (IEC 61131-3 extensible function) template inline auto CONCAT(const IECString& s1, const IECString& s2, const Args&... rest) noexcept { return CONCAT(CONCAT(s1, s2), rest...); } template inline IECString INSERT(const IECString& s1, const IECString& s2, size_t pos) noexcept { IECString result(s1); if (pos == 0) pos = 1; result.insert(pos - 1, s2.c_str()); return result; } template inline IECString DELETE_STR(const IECString& s, size_t len, size_t pos) noexcept { IECString result(s); if (pos == 0) pos = 1; result.erase(pos - 1, len); return result; } template inline IECString REPLACE(const IECString& s1, const IECString& s2, size_t len, size_t pos) noexcept { IECString result(s1); if (pos == 0) pos = 1; result.replace(pos - 1, len, s2.c_str()); return result; } // const char* overloads for string functions (codegen may emit string literals) template inline IECString REPLACE(const IECString& s1, const char* s2, size_t len, size_t pos) noexcept { return REPLACE(s1, IECString(s2), len, pos); } template inline IECString REPLACE(const IECStringVar& s1, const char* s2, size_t len, size_t pos) noexcept { return REPLACE(s1.get(), IECString(s2), len, pos); } template inline IECString INSERT(const IECString& s1, const char* s2, size_t pos) noexcept { return INSERT(s1, IECString(s2), pos); } template inline IECString INSERT(const IECStringVar& s1, const char* s2, size_t pos) noexcept { return INSERT(s1.get(), IECString(s2), pos); } template inline size_t FIND(const IECString& s1, const char* s2) noexcept { return FIND(s1, IECString(s2)); } template inline size_t FIND(const IECStringVar& s1, const char* s2) noexcept { return FIND(s1.get(), IECString(s2)); } template inline size_t FIND(const IECString& s1, const IECString& s2) noexcept { size_t pos = s1.find(s2); return pos == IECString::npos ? 0 : pos + 1; } // ============================================================================= // IECStringVar overloads — template deduction doesn't use implicit conversions, // so we need explicit overloads that forward to the IECString versions via .get() // ============================================================================= template inline IECString LEFT(const IECStringVar& s, size_t len) noexcept { return LEFT(s.get(), len); } template inline IECString RIGHT(const IECStringVar& s, size_t len) noexcept { return RIGHT(s.get(), len); } template inline IECString MID(const IECStringVar& s, size_t len, size_t pos) noexcept { return MID(s.get(), len, pos); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECStringVar& s1, const IECStringVar& s2) noexcept { return CONCAT(s1.get(), s2.get()); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECStringVar& s1, const IECString& s2) noexcept { return CONCAT(s1.get(), s2); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECString& s1, const IECStringVar& s2) noexcept { return CONCAT(s1, s2.get()); } template inline IECString INSERT(const IECStringVar& s1, const IECStringVar& s2, size_t pos) noexcept { return INSERT(s1.get(), s2.get(), pos); } template inline IECString INSERT(const IECStringVar& s1, const IECString& s2, size_t pos) noexcept { return INSERT(s1.get(), s2, pos); } template inline IECString INSERT(const IECString& s1, const IECStringVar& s2, size_t pos) noexcept { return INSERT(s1, s2.get(), pos); } template inline IECString DELETE_STR(const IECStringVar& s, size_t len, size_t pos) noexcept { return DELETE_STR(s.get(), len, pos); } template inline IECString REPLACE(const IECStringVar& s1, const IECStringVar& s2, size_t len, size_t pos) noexcept { return REPLACE(s1.get(), s2.get(), len, pos); } template inline IECString REPLACE(const IECStringVar& s1, const IECString& s2, size_t len, size_t pos) noexcept { return REPLACE(s1.get(), s2, len, pos); } template inline IECString REPLACE(const IECString& s1, const IECStringVar& s2, size_t len, size_t pos) noexcept { return REPLACE(s1, s2.get(), len, pos); } template inline size_t FIND(const IECStringVar& s1, const IECStringVar& s2) noexcept { return FIND(s1.get(), s2.get()); } template inline size_t FIND(const IECStringVar& s1, const IECString& s2) noexcept { return FIND(s1.get(), s2); } template inline size_t FIND(const IECString& s1, const IECStringVar& s2) noexcept { return FIND(s1, s2.get()); } // ============================================================================= // IECVar> overloads — struct field access returns this type. // Template deduction won't chain through operator T(). // ============================================================================= template inline size_t LEN(const IECVar>& s) noexcept { return static_cast>(s).length(); } template inline IECString LEFT(const IECVar>& s, size_t len) noexcept { return LEFT(static_cast>(s), len); } template inline IECString RIGHT(const IECVar>& s, size_t len) noexcept { return RIGHT(static_cast>(s), len); } template inline IECString MID(const IECVar>& s, size_t len, size_t pos) noexcept { return MID(static_cast>(s), len, pos); } template inline IECString DELETE_STR(const IECVar>& s, size_t len, size_t pos) noexcept { return DELETE_STR(static_cast>(s), len, pos); } template inline size_t FIND(const IECVar>& s1, const IECStringVar& s2) noexcept { return FIND(static_cast>(s1), s2.get()); } template inline size_t FIND(const IECVar>& s1, const IECString& s2) noexcept { return FIND(static_cast>(s1), s2); } template inline size_t FIND(const IECStringVar& s1, const IECVar>& s2) noexcept { return FIND(s1.get(), static_cast>(s2)); } template inline IECString REPLACE(const IECVar>& s1, const IECStringVar& s2, size_t len, size_t pos) noexcept { return REPLACE(static_cast>(s1), IECString(s2.get().c_str()), len, pos); } template inline IECString INSERT(const IECVar>& s1, const IECStringVar& s2, size_t pos) noexcept { return INSERT(static_cast>(s1), IECString(s2.get().c_str()), pos); } // Cross-size REPLACE/INSERT: arguments may have different string sizes template = 0> inline IECString REPLACE(const IECStringVar& s1, const IECStringVar& s2, size_t len, size_t pos) noexcept { return REPLACE(s1.get(), IECString(s2.get().c_str()), len, pos); } template = 0> inline IECString INSERT(const IECStringVar& s1, const IECStringVar& s2, size_t pos) noexcept { return INSERT(s1.get(), IECString(s2.get().c_str()), pos); } // CONCAT overloads for IECVar> template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECVar>& s1, const IECString& s2) noexcept { return CONCAT(static_cast>(s1), s2); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECString& s1, const IECVar>& s2) noexcept { return CONCAT(s1, static_cast>(s2)); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECVar>& s1, const IECStringVar& s2) noexcept { return CONCAT(static_cast>(s1), s2.get()); } template inline IECString<(MaxLen1 > MaxLen2 ? MaxLen1 : MaxLen2)> CONCAT(const IECStringVar& s1, const IECVar>& s2) noexcept { return CONCAT(s1.get(), static_cast>(s2)); } template inline bool GT_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 > s2; } template inline bool GE_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 >= s2; } template inline bool EQ_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 == s2; } template inline bool LE_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 <= s2; } template inline bool LT_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 < s2; } template inline bool NE_STRING(const IECString& s1, const IECString& s2) noexcept { return s1 != s2; } // ============================================================================= // TO_STRING Conversions // ============================================================================= // Numeric → STRING (uses snprintf for real-time safety, no std::to_string) // // Split into two SFINAE'd overloads (signed vs unsigned) rather than a single // function with `if constexpr`, because user C/C++ POU code in // `c_blocks_code.cpp` compiles under whatever -std= the platform's Arduino // core picked (gnu++14 on mbed cores). `if constexpr` is C++17-only and // would make this header unusable from those compilation units. Tag // dispatch via `enable_if_t` works back to C++11. template()))>::value && std::is_unsigned()))>::value, int> = 0> inline IECString<254> TO_STRING(T v) noexcept { char buf[32]; std::snprintf(buf, sizeof(buf), "%llu", static_cast(iec_unwrap(v))); return IECString<254>(buf); } template()))>::value && !std::is_unsigned()))>::value, int> = 0> inline IECString<254> TO_STRING(T v) noexcept { char buf[32]; std::snprintf(buf, sizeof(buf), "%lld", static_cast(iec_unwrap(v))); return IECString<254>(buf); } template()))>::value, int> = 0> inline IECString<254> TO_STRING(T v) noexcept { char buf[64]; std::snprintf(buf, sizeof(buf), "%g", static_cast(iec_unwrap(v))); return IECString<254>(buf); } // BOOL → STRING inline IECString<254> TO_STRING(IECVar v) noexcept { return IECString<254>(iec_unwrap(v) ? "TRUE" : "FALSE"); } // IECString → STRING (identity / cross-size copy) template inline IECString<254> TO_STRING(const IECString& v) noexcept { return IECString<254>(v.c_str()); } template inline IECString<254> TO_STRING(const IECStringVar& v) noexcept { return IECString<254>(v.get().c_str()); } // ============================================================================= // OSCAT-Compatible String Functions // ============================================================================= // CODE - Return ASCII code of first character template inline int32_t CODE(const IECString& s) noexcept { return s.length() > 0 ? static_cast(static_cast(s[0])) : 0; } template inline int32_t CODE(const IECStringVar& s) noexcept { return CODE(s.get()); } // CHR - Return string from ASCII code inline IECString<254> CHR(int32_t code) noexcept { char buf[2] = { static_cast(code), '\0' }; return IECString<254>(buf); } // TRIM - Remove leading and trailing whitespace template inline IECString TRIM(const IECString& s) noexcept { const char* start = s.c_str(); const char* end = start + s.length(); while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) ++start; while (end > start && (*(end-1) == ' ' || *(end-1) == '\t' || *(end-1) == '\r' || *(end-1) == '\n')) --end; return IECString(start, static_cast(end - start)); } template inline IECString TRIM(const IECStringVar& s) noexcept { return TRIM(s.get()); } // LOWERCASE / TOUPPER - Case conversion (OSCAT uses these names) template inline IECString LOWERCASE(const IECString& s) noexcept { IECString result(s); for (size_t i = 0; i < result.length(); ++i) { char c = result[i]; if (c >= 'A' && c <= 'Z') result[i] = c + ('a' - 'A'); } return result; } template inline IECString LOWERCASE(const IECStringVar& s) noexcept { return LOWERCASE(s.get()); } template inline IECString UPPERCASE(const IECString& s) noexcept { IECString result(s); for (size_t i = 0; i < result.length(); ++i) { char c = result[i]; if (c >= 'a' && c <= 'z') result[i] = c - ('a' - 'A'); } return result; } template inline IECString UPPERCASE(const IECStringVar& s) noexcept { return UPPERCASE(s.get()); } // CONCAT with const char* overloads (codegen may mix string literals with IECString) template inline IECString CONCAT(const IECString& s1, const char* s2) noexcept { IECString result(s1); result.append(s2); return result; } template inline IECString CONCAT(const char* s1, const IECString& s2) noexcept { IECString result(s1); result.append(s2); return result; } template inline IECString CONCAT(const IECStringVar& s1, const char* s2) noexcept { return CONCAT(s1.get(), s2); } template inline IECString CONCAT(const char* s1, const IECStringVar& s2) noexcept { return CONCAT(s1, s2.get()); } // ============================================================================= // String-to-Numeric Conversions // IEC 61131-3: STRING_TO_INT, STRING_TO_REAL, etc. // Placed here because they need both IECString and IEC_INT/IEC_REAL types. // ============================================================================= template inline IEC_INT TO_INT(const IECString& s) noexcept { return IEC_INT(static_cast(std::strtol(s.c_str(), nullptr, 10))); } template inline IEC_INT TO_INT(const IECStringVar& s) noexcept { return TO_INT(s.get()); } template inline IEC_REAL TO_REAL(const IECString& s) noexcept { return IEC_REAL(static_cast(std::strtod(s.c_str(), nullptr))); } template inline IEC_REAL TO_REAL(const IECStringVar& s) noexcept { return TO_REAL(s.get()); } template inline IEC_LREAL TO_LREAL(const IECString& s) noexcept { return IEC_LREAL(static_cast(std::strtod(s.c_str(), nullptr))); } template inline IEC_LREAL TO_LREAL(const IECStringVar& s) noexcept { return TO_LREAL(s.get()); } template inline IEC_DINT TO_DINT(const IECString& s) noexcept { return IEC_DINT(static_cast(std::strtol(s.c_str(), nullptr, 10))); } template inline IEC_DINT TO_DINT(const IECStringVar& s) noexcept { return TO_DINT(s.get()); } template inline IEC_SINT TO_SINT(const IECString& s) noexcept { return IEC_SINT(static_cast(std::strtol(s.c_str(), nullptr, 10))); } template inline IEC_SINT TO_SINT(const IECStringVar& s) noexcept { return TO_SINT(s.get()); } template inline IEC_LINT TO_LINT(const IECString& s) noexcept { return IEC_LINT(static_cast(std::strtoll(s.c_str(), nullptr, 10))); } template inline IEC_LINT TO_LINT(const IECStringVar& s) noexcept { return TO_LINT(s.get()); } template inline IEC_USINT TO_USINT(const IECString& s) noexcept { return IEC_USINT(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_USINT TO_USINT(const IECStringVar& s) noexcept { return TO_USINT(s.get()); } template inline IEC_UINT TO_UINT(const IECString& s) noexcept { return IEC_UINT(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_UINT TO_UINT(const IECStringVar& s) noexcept { return TO_UINT(s.get()); } template inline IEC_UDINT TO_UDINT(const IECString& s) noexcept { return IEC_UDINT(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_UDINT TO_UDINT(const IECStringVar& s) noexcept { return TO_UDINT(s.get()); } template inline IEC_ULINT TO_ULINT(const IECString& s) noexcept { return IEC_ULINT(static_cast(std::strtoull(s.c_str(), nullptr, 10))); } template inline IEC_ULINT TO_ULINT(const IECStringVar& s) noexcept { return TO_ULINT(s.get()); } template inline IEC_BYTE TO_BYTE(const IECString& s) noexcept { return IEC_BYTE(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_BYTE TO_BYTE(const IECStringVar& s) noexcept { return TO_BYTE(s.get()); } template inline IEC_WORD TO_WORD(const IECString& s) noexcept { return IEC_WORD(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_WORD TO_WORD(const IECStringVar& s) noexcept { return TO_WORD(s.get()); } template inline IEC_DWORD TO_DWORD(const IECString& s) noexcept { return IEC_DWORD(static_cast(std::strtoul(s.c_str(), nullptr, 10))); } template inline IEC_DWORD TO_DWORD(const IECStringVar& s) noexcept { return TO_DWORD(s.get()); } template inline IEC_LWORD TO_LWORD(const IECString& s) noexcept { return IEC_LWORD(static_cast(std::strtoull(s.c_str(), nullptr, 10))); } template inline IEC_LWORD TO_LWORD(const IECStringVar& s) noexcept { return TO_LWORD(s.get()); } template inline IEC_BOOL TO_BOOL(const IECString& s) noexcept { // "TRUE" or "1" → true, everything else → false return IEC_BOOL(s.length() > 0 && (s[0] == 'T' || s[0] == 't' || s[0] == '1')); } template inline IEC_BOOL TO_BOOL(const IECStringVar& s) noexcept { return TO_BOOL(s.get()); } } // namespace strucpp