使用sscanf读取double时忽略'E'字符。

14

我有这样的输入:"(50.1003781N, 14.3925125E)" 。这是纬度和经度。

我想要使用

sscanf(string,"(%lf%c, %lf%c)",&a,&b,&c,&d);

但是当%lf在数字后面看到E时,它会将其消耗并将其存储为指数形式的数字。有没有办法禁用这个功能?

但是当%lf在数字后面看到E时,它会将其消耗并将其存储为指数形式的数字。有没有办法禁用这个功能?


一个选项是将输入字符串分解为子字符串,然后从那里进行扫描。 - dwcanillas
1
你可以用一个占位符替换 E,然后如果它有这个占位符,就将 bd 替换为 E - NathanOliver
2
你能否移动,使得你所有的经度都是W而不是E? - Jonathan Leffler
1
不确定我是否理解您的问题,我将在稍后读取这些坐标,如果是W,则将其存储为负双精度数,如果是E,则将其存储为正数。 - lllook
四月份,我在SO 29381290上发表评论,指出C++代码存在问题。自从发表评论以来(我现在已经删除了评论),代码已经全面修复。但它仍然是C++代码。我的答案是C代码;它可以轻松升级为C++代码。(主要问题是goto error;语句跳过了int c = toupper((unsigned char)*end++); - 次要问题是使用了C风格的转换,如(unsigned char)*end++。) - Jonathan Leffler
显示剩余5条评论
3个回答

5
我认为你需要进行手动解析,可能需要使用 strtod()。这表明在处理结尾的 E 时,strtod() 的行为是合理的(至少在 Mac OS X 10.10.3 和 GCC 4.9.1 上是如此,但很可能适用于任何地方)。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    const char latlong[] = "(50.1003781N, 14.3925125E)";
    char *eptr;
    double d;
    errno = 0;      // Necessary in general, but probably not necessary at this point
    d = strtod(&latlong[14], &eptr);
    if (eptr != &latlong[14])
        printf("PASS: %10.7f (%s)\n", d, eptr);
    else
        printf("FAIL: %10.7f (%s) - %d: %s\n", d, eptr, errno, strerror(errno));

    return 0;
}

编译和运行:

$ gcc -O3 -g -std=c11 -Wall -Wextra -Werror latlong.c -o latlong
$ ./latlong
PASS: 14.3925125 (E))
$

基本上,你将跳过空格,检查是否有(strtod()一个数字,检查NS或小写版本,逗号,strtod()一个数字,检查WE,检查),可能在其前允许空格。
升级的代码,具有基于strtod()等的中等通用的strtolatlon()函数。在诸如strtod()之类的函数中,'const cast'是必要的,该函数接受const char *输入,并通过char **eptr变量返回指向该字符串的指针。
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CONST_CAST(type, value) ((type)(value))

extern int strtolatlon(const char *str, double *lat, double *lon, char **eptr);

int strtolatlon(const char *str, double *lat, double *lon, char **eptr)
{
    const char *s = str;
    char *end;
    while (isspace(*s))
        s++;
    if (*s != '(')
        goto error;
    *lat = strtod(++s, &end);
    if (s == end || *lat > 90.0 || *lat < 0.0)
        goto error;
    int c = toupper((unsigned char)*end++);
    if (c != 'N' && c != 'S')  // I18N
        goto error;
    if (c == 'S')
        *lat = -*lat;
    if (*end != ',')
        goto error;
    s = end + 1;
    *lon = strtod(s, &end);
    if (s == end || *lon > 180.0 || *lon < 0.0)
        goto error;
    c = toupper((unsigned char)*end++);
    if (c != 'W' && c != 'E')  // I18N
        goto error;
    if (c == 'E')
        *lon = -*lon;
    if (*end != ')')
        goto error;
    if (eptr != 0)
        *eptr = end + 1;
    return 0;

error:
    if (eptr != 0)
        *eptr = CONST_CAST(char *, str);
    errno = EINVAL;
    return -1;
}

int main(void)
{
    const char latlon1[] = "(50.1003781N, 14.3925125E)";
    const char latlon2[] = "   (50.1003781N, 14.3925125E) is the position!";
    char *eptr;
    double d;
    errno = 0;      // Necessary in general, but Probably not necessary at this point
    d = strtod(&latlon1[14], &eptr);
    if (eptr != &latlon1[14])
        printf("PASS: %10.7f (%s)\n", d, eptr);
    else
        printf("FAIL: %10.7f (%s) - %d: %s\n", d, eptr, errno, strerror(errno));

    printf("Converting <<%s>>\n", latlon2);
    double lat;
    double lon;
    int rc = strtolatlon(latlon2, &lat, &lon, &eptr);
    if (rc == 0)
        printf("Lat: %11.7f, Lon: %11.7f; trailing material: <<%s>>\n", lat, lon, eptr);
    else
        printf("Conversion failed\n");

    return 0;
}

样例输出:

PASS: 14.3925125 (E))
Converting <<   (50.1003781N, 14.3925125E) is the position!>>
Lat:  50.1003781, Lon: -14.3925125; trailing material: << is the position!>>

那不是全面的测试,但它是说明性的,并且接近生产质量。例如,在真正的生产代码中,您可能需要担心无穷大的问题。我不经常使用goto,但这是一种情况,其中使用goto简化了错误处理。你可以不用它来编写代码;如果我有更多时间,也许我会升级它。然而,由于有7个地方诊断错误并且需要4行报告错误,使用goto提供了合理的清晰度而不会重复太多。
请注意,strtolatlon()函数通过其返回值明确标识错误;没有必要猜测它是否成功。如果您希望确定错误的位置,则可以增强错误报告。但是,这样做取决于您的错误报告基础设施,而本文不涉及此问题。
此外,strtolatlon() 函数将接受一些奇怪的格式,例如 (+0.501003781E2N, 143925125E-7E)。如果这是个问题,你需要编写自己更加挑剔的 strtod() 变体,只接受定点表示法。另一方面,有一个meme/guideline“在接受方面要慷慨;在生成方面要严格”的规则。这意味着这里的内容或多或少都可以(在 N、S、E、W 字母、逗号和右括号之前允许可选的空格可能会更好)。相反的代码 latlontostr()fmt_latlon()(将 strtolatlon() 重命名为 scn_latlon(),也许)等会仔细考虑它生成的内容,只生成大写字母,并始终使用固定格式等。
int fmt_latlon(char *buffer, size_t buflen, double lat, double lon, int dp)
{
    assert(dp >= 0 && dp < 15);
    assert(lat >=  -90.0 && lat <=  90.0);
    assert(lon >= -180.0 && lon <= 180.0);
    assert(buffer != 0 && buflen != 0);
    char ns = 'N';
    if (lat < 0.0)
    {
        ns = 'S';
        lat = -lat;
    }
    char ew = 'W';
    if (lon < 0.0)
    {
        ew = 'E';
        lon = -lon;
    }
    int nbytes = snprintf(buffer, buflen, "(%.*f%c, %.*f%c)", dp, lat, ns, dp, lon, ew);
    if (nbytes < 0 || (size_t)nbytes >= buflen)
        return -1;
    return 0;
}

请注意,1个7位小数的度(10-7 ˚)对应于地面上约1厘米的距离(沿子午线方向定向;纬度平行线上的一度距离当然随着纬度而变化)。

4

首先使用以下方式处理字符串

char *p;
while((p = strchr(string, 'E')) != NULL) *p = 'W';
while((p = strchr(string, 'e')) != NULL) *p = 'W';

// scan it using your approach

sscanf(string,"(%lf%c, %lf%c)",&a,&b,&c,&d);

// get back the original characters (converted to uppercase).

if (b == 'W') b = 'E';    
if (d == 'W') d = 'E';

strchr()在C头文件<string.h>中声明。

注意:这实际上是一种C方法,而不是C++方法。但是,通过使用sscanf(),您实际上正在使用C方法。


谢谢,那么C++的方法是什么?这是C++项目的一部分,我只能想到尝试使用sscanf来解决这个问题。 - lllook
4
使用'W'可能不是最好的选择。如果我要往西走怎么办? - Quentin
至少,您需要记录是否将 Ee 更改为 W,以便在使用 sscanf() 后可以撤消更改。您可能还想记录原始字符,以便可以将字符串恢复为原样。当然,这种方法也意味着您无法扫描包含纬度/经度的字符串文字。 - Jonathan Leffler
好的,重点是改变字符以进行扫描,并记录足够的信息,以便可以检索值(例如,如果更改E-W,则乘以-1)。有效坐标将不具有不同的东西经度。在尝试扫描坐标之前和之后都需要进行整个错误检查(仅检查字符串包含一对坐标,检查从sscanf()返回的内容等等)。 - Peter
使用 C++ 方法可以利用 std::string 类型(可能不会将变量命名为 "string")来处理。std::string 支持多种操作,例如替换子字符串等。扫描/解析(而非 sscanf())的一种方法是将字符串提供给 std::istringstream 并使用它来提取值。 - Peter

0

你可以尝试读取整个字符串,然后用另一个字符替换 E。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接