diff --git a/.gitignore b/.gitignore
index a1bbe1eab8..7de6e416f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,243 +1,244 @@
 # Common
 \#*
 .\#*
 GPATH
 GRTAGS
 GTAGS
 TAGS
 Makefile
 Makefile.in
 .deps
 .dirstamp
 .libs
 *.pc
 *.pyc
 *.bz2
 *.tar.gz
 *.tgz
 *.la
 *.lo
 *.o
 *~
 *.gcda
 *.gcno
 
 
 # Autobuild
 aclocal.m4
 autoconf
 autoheader
 autom4te.cache/
 automake
 build.counter
 compile
 /confdefs.h
 config.guess
 config.log
 config.status
 config.sub
 configure
 /conftest*
 depcomp
 install-sh
 include/stamp-*
 libtool
 libtool.m4
 ltdl.m4
 libltdl
 ltmain.sh
 missing
 py-compile
 /m4/argz.m4
 /m4/ltargz.m4
 /m4/ltoptions.m4
 /m4/ltsugar.m4
 /m4/ltversion.m4
 /m4/lt~obsolete.m4
 test-driver
 ylwrap
 
 # Configure targets
 /cts/CTS.py
 /cts/CTSlab.py
 /cts/CTSvars.py
 /cts/LSBDummy
 /cts/OCFIPraTest.py
 /cts/benchmark/clubench
 /cts/cluster_test
 /cts/cts
 /cts/cts-cli
 /cts/cts-coverage
 /cts/cts-exec
 /cts/cts-fencing
 /cts/cts-log-watcher
 /cts/cts-regression
 /cts/cts-scheduler
 /cts/cts-support
 /cts/fence_dummy
 /cts/lxc_autogen.sh
 /cts/pacemaker-cts-dummyd
 /cts/pacemaker-cts-dummyd@.service
 /daemons/execd/pacemaker_remote
 /daemons/execd/pacemaker_remote.service
 /daemons/fenced/fence_legacy
 /daemons/pacemakerd/pacemaker
 /daemons/pacemakerd/pacemaker.combined.upstart
 /daemons/pacemakerd/pacemaker.service
 /daemons/pacemakerd/pacemaker.upstart
 /doc/Doxyfile
 /extra/logrotate/pacemaker
 /extra/resources/ClusterMon
 /extra/resources/HealthSMART
 /extra/resources/SysInfo
 /extra/resources/ifspeed
 /extra/resources/o2cb
 include/config.h
 include/config.h.in
 include/crm_config.h
 publican.cfg
 /tools/cibsecret
 /tools/crm_error
 /tools/crm_failcount
 /tools/crm_master
 /tools/crm_mon.service
 /tools/crm_mon.upstart
 /tools/crm_report
 /tools/crm_rule
 /tools/crm_standby
 /tools/pcmk_simtimes
 /tools/report.collector
 /tools/report.common
 
 # Build targets
 *.7
 *.7.xml
 *.7.html
 *.8
 *.8.xml
 *.8.html
 /daemons/attrd/pacemaker-attrd
 /daemons/based/pacemaker-based
 /daemons/based/cibmon
 /daemons/controld/pacemaker-controld
 /daemons/execd/cts-exec-helper
 /daemons/execd/pacemaker-execd
 /daemons/execd/pacemaker-remoted
 /daemons/fenced/cts-fence-helper
 /daemons/fenced/pacemaker-fenced
 /daemons/fenced/pacemaker-fenced.xml
 /daemons/pacemakerd/pacemakerd
 /daemons/schedulerd/pacemaker-schedulerd
 /daemons/schedulerd/pacemaker-schedulerd.xml
 /doc/*/tmp/**
 /doc/*/publish
 /doc/*.build
 /doc/*/en-US/Ap-*.xml
 /doc/*/en-US/Ch-*.xml
 /doc/.ABI-build
 /doc/HTML
 /doc/abi_dumps
 /doc/abi-check
 /doc/api/*
 /doc/compat_reports
 /doc/crm_fencing.html
 /doc/publican-catalog*
 /doc/shared/en-US/*.xml
 /doc/shared/en-US/images/pcmk-*.png
 /doc/shared/en-US/images/Policy-Engine-*.png
 /doc/sphinx/*/_build
 /doc/sphinx/*/conf.py
 /lib/common/md5.c
 /maint/testcc_helper.cc
 /maint/testcc_*_h
 /maint/mocked/based
 scratch
 /tools/attrd_updater
 /tools/cibadmin
 /tools/crmadmin
 /tools/crm_attribute
 /tools/crm_diff
 /tools/crm_mon
 /tools/crm_node
 /tools/crm_resource
 /tools/crm_shadow
 /tools/crm_simulate
 /tools/crm_ticket
 /tools/crm_verify
 /tools/iso8601
 /tools/stonith_admin
 xml/crm.dtd
 xml/pacemaker*.rng
 xml/versions.rng
 xml/api/api-result*.rng
 lib/gnu/libgnu.a
 lib/gnu/stdalign.h
 *.coverity
 
 # Packager artifacts
 *.rpm
 /mock
 /pacemaker.spec
 /rpm/[A-Z]*
 
 # make dist/export working directory
 pacemaker-[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]
 
 # Test detritus
 /cts/.regression.failed.diff
 /cts/scheduler/*.ref
 /cts/scheduler/*.up
 /cts/scheduler/*.up.err
 /cts/scheduler/bug-rh-1097457.log
 /cts/scheduler/bug-rh-1097457.trs
 /cts/scheduler/shadow.*
 /cts/test-suite.log
 /xml/test-*/*.up
 /xml/test-*/*.up.err
 /xml/assets/*.rng
 /xml/assets/diffview.js
 /xml/assets/xmlcatalog
 
 # Test results
 *.log
 *.trs
+/lib/common/tests/strings/pcmk__scan_double
 /lib/common/tests/strings/pcmk__parse_ll_range
 /lib/common/tests/strings/pcmk__str_any_of
 /lib/common/tests/strings/pcmk__strcmp
 /lib/common/tests/utils/pcmk_str_is_infinity
 /lib/common/tests/utils/pcmk_str_is_minus_infinity
 /lib/pengine/tests/rules/pe_cron_range_satisfied
 
 # Release maintenance detritus
 /maint/gnulib
 
 # Formerly built files (helps when jumping back and forth in checkout)
 /.ABI-build
 /Doxyfile
 /HTML
 /abi_dumps
 /abi-check
 /compat_reports
 /attrd
 /cib
 /coverage.sh
 /crmd
 /cts/HBDummy
 /doc/Clusters_from_Scratch.txt
 /doc/Pacemaker_Explained.txt
 /doc/acls.html
 /fencing
 /lrmd
 /mcp
 /pacemaker-*.spec
 /pengine
 
 #Other 
 coverity-*
 logs
 *.patch
 *.diff
 *.sed
 *.orig
 *.rej
 *.swp
diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am
index 11c3bd3c00..5c5e29181e 100644
--- a/lib/common/tests/strings/Makefile.am
+++ b/lib/common/tests/strings/Makefile.am
@@ -1,21 +1,22 @@
 AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_builddir)/include
 LDADD = $(top_builddir)/lib/common/libcrmcommon.la
 
 include $(top_srcdir)/mk/glib-tap.mk
 
 # Add each test program here.  Each test should be written as a little standalone
 # program using the glib unit testing functions.  See the documentation for more
 # information.
 #
 # https://developer.gnome.org/glib/unstable/glib-Testing.html
-test_programs = pcmk__parse_ll_range \
+test_programs = pcmk__scan_double \
+				pcmk__parse_ll_range \
 				pcmk__str_any_of \
 				pcmk__strcmp
 
 # If any extra data needs to be added to the source distribution, add it to the
 # following list.
 dist_test_data =
 
 # If any extra data needs to be used by tests but should not be added to the
 # source distribution, add it to the following list.
 test_data =
diff --git a/lib/common/tests/strings/pcmk__scan_double.c b/lib/common/tests/strings/pcmk__scan_double.c
new file mode 100644
index 0000000000..1e6138a2e6
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__scan_double.c
@@ -0,0 +1,161 @@
+#include <float.h>  // DBL_MAX, etc.
+#include <math.h>   // fabs()
+
+#include <glib.h>
+
+#include <crm_internal.h>
+
+// Ensure plenty of characters for %f display
+#define LOCAL_BUF_SIZE 2 * DBL_MAX_10_EXP
+
+/*
+ * Avoids compiler warnings for floating-point equality checks.
+ * Use for comparing numbers (e.g., 1.0 == 1.0), not expression values.
+ */
+#define ASSERT_DBL_EQ(d1, d2) g_assert_cmpfloat(fabs(d1 - d2), \
+                                                <, DBL_EPSILON);
+
+static void
+empty_input_string(void)
+{
+    double result;
+
+    // Without default_text
+    g_assert_cmpint(pcmk__scan_double(NULL, &result, NULL, NULL), ==, EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+
+    g_assert_cmpint(pcmk__scan_double("", &result, NULL, NULL), ==, EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+
+    // With default_text
+    g_assert_cmpint(pcmk__scan_double(NULL, &result, "2.0", NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, 2.0);
+
+    g_assert_cmpint(pcmk__scan_double("", &result, "2.0", NULL), ==, EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+}
+
+static void
+bad_input_string(void)
+{
+    double result;
+
+    // Without default text
+    g_assert_cmpint(pcmk__scan_double("asdf", &result, NULL, NULL), ==, EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+
+    g_assert_cmpint(pcmk__scan_double("as2.0", &result, NULL, NULL), ==,
+                    EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+
+    // With default text (not used)
+    g_assert_cmpint(pcmk__scan_double("asdf", &result, "2.0", NULL), ==,
+                    EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+
+    g_assert_cmpint(pcmk__scan_double("as2.0", &result, "2.0", NULL), ==,
+                    EINVAL);
+    ASSERT_DBL_EQ(result, PCMK__PARSE_DBL_DEFAULT);
+}
+
+static void
+trailing_chars(void)
+{
+    double result;
+
+    g_assert_cmpint(pcmk__scan_double("2.0asdf", &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, 2.0);
+}
+
+static void
+typical_case(void)
+{
+    char str[LOCAL_BUF_SIZE];
+    double result;
+
+    g_assert_cmpint(pcmk__scan_double("0.0", &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, 0.0);
+
+    g_assert_cmpint(pcmk__scan_double("1.0", &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, 1.0);
+
+    g_assert_cmpint(pcmk__scan_double("-1.0", &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, -1.0);
+
+    snprintf(str, LOCAL_BUF_SIZE, "%f", DBL_MAX);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, DBL_MAX);
+
+    snprintf(str, LOCAL_BUF_SIZE, "%f", -DBL_MAX);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==,
+                    pcmk_rc_ok);
+    ASSERT_DBL_EQ(result, -DBL_MAX);
+}
+
+static void
+double_overflow(void)
+{
+    char str[LOCAL_BUF_SIZE];
+    double result;
+
+    /*
+     * 1e(DBL_MAX_10_EXP + 1) produces an inf value
+     * Can't use ASSERT_DBL_EQ() because (inf - inf) == NaN
+     */
+    snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MAX_10_EXP + 1);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==, EOVERFLOW);
+    g_assert_cmpfloat(result, >, DBL_MAX);
+
+    snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MAX_10_EXP + 1);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==, EOVERFLOW);
+    g_assert_cmpfloat(result, <, -DBL_MAX);
+}
+
+static void
+double_underflow(void)
+{
+    char str[LOCAL_BUF_SIZE];
+    double result;
+
+    /*
+     * 1e(DBL_MIN_10_EXP - 1) produces a denormalized value (between 0
+     * and DBL_MIN)
+     *
+     * C99/C11: result will be **no greater than** DBL_MIN
+     */
+    snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MIN_10_EXP - 1);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==,
+                    pcmk_rc_underflow);
+    g_assert_cmpfloat(result, >=, 0.0);
+    g_assert_cmpfloat(result, <=, DBL_MIN);
+
+    snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MIN_10_EXP - 1);
+    g_assert_cmpint(pcmk__scan_double(str, &result, NULL, NULL), ==,
+                    pcmk_rc_underflow);
+    g_assert_cmpfloat(result, <=, 0.0);
+    g_assert_cmpfloat(result, >=, -DBL_MIN);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    // Test for input string issues
+    g_test_add_func("/common/strings/double/empty_input", empty_input_string);
+    g_test_add_func("/common/strings/double/bad_input", bad_input_string);
+    g_test_add_func("/common/strings/double/trailing_chars", trailing_chars);
+
+    // Test for numeric issues
+    g_test_add_func("/common/strings/double/typical", typical_case);
+    g_test_add_func("/common/strings/double/overflow", double_overflow);
+    g_test_add_func("/common/strings/double/underflow", double_underflow);
+
+    return g_test_run();
+}
+