C, 232
This of course uses much more code, but I think the spirit of the problem exonerates this bare-bones approach; for all we know, the internal rationalize() functions of modern languages have lots of internal loops.
Note that this doesn't work for an input of "0." because that is not a standard way to write a float, so when it re-writes the float to string, the result will never be a "0.".
The specs want a function that returns values instead of just printing to screen, hence the argument-passing.
Code (ungolfed):
void r(char* x, int* a, int* b) {
int i = -1;
char z[32];
double v =atof(x);
while(1) {
i++;
double y = ((double)i)/((double)(*b));
double w;
sprintf(z, "%.*f", strlen(strchr(x,'.'))-1, y);
if(strcmp(x, z)==0) {
*a = i;
return;
}
w = atof(z);
if(w > v) {
(*b)++;
r(x, a, b);
return;
}
}
}
Usage:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
int num;
int denom = 1; // start with a denominator of 1
r(argv[1], &num, &denom);
printf("%d/%d\n", num, denom);
return 0;
}
Golfed code:
#define double D
void r(char*x,int*a,int*b){int i=-1;char z[32];D v=atof(x);while(1){i++;D y=((D)i)/((D)(*b));D w;sprintf(z,"%.*f",strlen(strchr(x,'.'))-1,y);if(!strcmp(x,z)){*a=i;return;}w=atof(z);if(w>v){(*b)++;r(x,a,b);return;}}}